using System.Collections; using System.Collections.Generic; using UnityEngine; namespace BulletHellTemplate { /// /// Manages the gameplay, including the survival timer, spawning waves of monsters, handling perk levels, and pooling system. /// public class GameplayManager : MonoBehaviour { public static GameplayManager Singleton { get; private set; } public enum WinCondition { SurvivalTime, // Win by surviving until the time runs out KillBoss, // Win by killing a specific boss monster within the time limit SurvivalTimeAndKillBoss // Win by surviving until the time runs out and then killing the final boss } public enum UpgradeMode { UpgradeOnLevelUp, UpgradeOnButtonClick, UpgradeRandom } public WinCondition winCondition; // The selected win condition for this game public UpgradeMode upgradeMode = UpgradeMode.UpgradeOnLevelUp; public MonsterEntity bossPrefab; // The boss to spawn in KillBoss or SurvivalTimeAndKillBoss modes public int survivalTime = 600; // Survival time in seconds (10 minutes) public List waves; // List of configured waves private int currentWaveIndex = 0; // Index of the current wave private float timeRemaining; // Time remaining for the next wave or win condition private bool gameRunning = true; // Control the game state private bool isPaused = false; // Tracks if the game is paused private float waveStartTime; // Time when the current wave started private Coroutine waveSpawner; // Reference to the wave spawning coroutine private int monstersKilled; // Tracks the number of monsters killed private int goldGain; // Tracks the total gold gained private int xpGain; // Tracks the total XP gained public int[] xpToNextLevel; // Array to store XP required for each level public int maxLevel; // Maximum level a character can reach public int minDamage = 5; [Header("Perks Settings")] public int maxLevelStatPerks = 5; public int maxLevelSkillPerks = 5; public int maxSkills = 5; public int maxStats = 5; public List spawnPointsList; // List of available spawn points [HideInInspector] public CharacterEntity character; private Dictionary perkLevels = new Dictionary(); private Dictionary skillLevels = new Dictionary(); // Tracks the levels of each skill private Dictionary skillBaseLevels = new Dictionary(); // Tracks levels of base skills public enum TimeFormat { Seconds, MinutesSeconds } public SkillPerkData[] skillPerkData; public StatPerkData[] statPerkData; public TimeFormat timeDisplayFormat = TimeFormat.Seconds; // Default time display format [Header("Pooling Settings")] [Tooltip("Enable pooling system for mobs.")] public bool usePooling = true; private Dictionary> mobPools = new Dictionary>(); private int totalMobsToSpawn = 0; // Total number of mobs to pre-instantiate private int mobsInstantiated = 0; // Mobs instantiated so far private bool isPoolingReady = false; // Indicates if pooling is completed private void Awake() { // Implement singleton pattern if (Singleton == null) { Singleton = this; } else { Destroy(gameObject); } // Initialize perk levels dictionary perkLevels = new Dictionary(); } void Start() { StartCoroutine(FindCharacterEntity()); timeRemaining = survivalTime; // Start the pooling process if (usePooling) { StartCoroutine(InitializePooling()); } else { isPoolingReady = true; StartGameplay(); // Start the game immediately if pooling is not used } } /// /// Coroutine that finds the CharacterEntity in the scene and sets up related gameplay settings. /// /// An IEnumerator for coroutine execution. private IEnumerator FindCharacterEntity() { // Loop until the CharacterEntity is found in the scene while (character == null) { character = FindObjectOfType(); if (character == null) { yield return new WaitForSeconds(0.1f); // Wait for a short period before retrying } } if (character.GetCharacterData().autoAttack != null) { var aaSkill = character.GetCharacterData().autoAttack; if (!skillBaseLevels.ContainsKey(aaSkill)) { skillBaseLevels[aaSkill] = 0; } } foreach (var skill in character.GetCharacterData().skills) { if (!skillBaseLevels.ContainsKey(skill)) { skillBaseLevels[skill] = 0; } } // Ensure that the UIGameplay Singleton is initialized while (UIGameplay.Singleton == null) { yield return new WaitForSeconds(0.1f); } // Now that the character is found, set maxSkills and maxStats based on the character's current stats maxSkills = (int)character.GetCurrentMaxSkills(); maxStats = (int)character.GetCurrentMaxStats(); // Update the UI to reflect the new perk settings UIGameplay.Singleton.UpdateSkillPerkUI(); UIGameplay.Singleton.UpdateStatPerkUI(); } public CharacterEntity GetCharacterEntity() { return character; } /// /// Coroutine to initialize the pooling system by pre-instantiating mobs. /// Calculates the total number of mobs needed and instantiates them. /// /// An IEnumerator for coroutine execution. private IEnumerator InitializePooling() { // Calculate total mobs needed CalculateTotalMobsToSpawn(); // Instantiate mobs foreach (Wave wave in waves) { foreach (MonsterConfig monsterConfig in wave.monsters) { MonsterEntity monsterPrefab = monsterConfig.monsterPrefab; int mobsNeeded = Mathf.FloorToInt(wave.waveDuration / monsterConfig.spawnInterval); if (!mobPools.ContainsKey(monsterPrefab)) { mobPools[monsterPrefab] = new Queue(); } for (int i = 0; i < mobsNeeded; i++) { MonsterEntity mobInstance = Instantiate(monsterPrefab); mobInstance.gameObject.SetActive(false); mobPools[monsterPrefab].Enqueue(mobInstance); mobsInstantiated++; yield return null; // Yield to allow loading progress to update } } } isPoolingReady = true; StartGameplay(); // Start the game after pooling is ready } /// /// Starts the gameplay by initiating the survival countdown and spawning waves. /// private void StartGameplay() { StartCoroutine(SurvivalCountdown()); waveSpawner = StartCoroutine(SpawnWaves()); } /// /// Checks if the pooling is ready. /// /// True if pooling is ready; otherwise, false. public bool IsPoolingReady() { return isPoolingReady; } /// /// Calculates the total number of mobs that need to be spawned across all waves. /// private void CalculateTotalMobsToSpawn() { totalMobsToSpawn = 0; foreach (Wave wave in waves) { foreach (MonsterConfig monsterConfig in wave.monsters) { int mobsNeeded = Mathf.FloorToInt(wave.waveDuration / monsterConfig.spawnInterval); totalMobsToSpawn += mobsNeeded; } } } /// /// Returns the current loading progress percentage of the pooling system. /// /// A float value between 0 and 1 representing the loading progress. public float GetLoadingProgress() { if (totalMobsToSpawn == 0) return 1f; return (float)mobsInstantiated / totalMobsToSpawn; } IEnumerator SurvivalCountdown() { while (timeRemaining > 0 && gameRunning) { if (!isPaused) { yield return new WaitForSeconds(1f); timeRemaining--; } else { yield return null; // Wait for next frame while paused } } if (gameRunning && (winCondition == WinCondition.SurvivalTime || winCondition == WinCondition.SurvivalTimeAndKillBoss)) { HandleWinCondition(); } } IEnumerator SpawnWaves() { // Wait until pooling is ready while (usePooling && !isPoolingReady) { yield return null; } while (currentWaveIndex < waves.Count && gameRunning) { Wave currentWave = waves[currentWaveIndex]; waveStartTime = Time.time; List nextSpawnTimes = new List(new float[currentWave.monsters.Count]); List remainingSpawns = new List(new int[currentWave.monsters.Count]); // Initialize spawn times for each monster configuration and set remaining spawns to initial value for (int i = 0; i < currentWave.monsters.Count; i++) { nextSpawnTimes[i] = waveStartTime + currentWave.monsters[i].spawnInterval; remainingSpawns[i] = Mathf.FloorToInt(currentWave.waveDuration / currentWave.monsters[i].spawnInterval); // Calculate total possible spawns in the wave duration } // Continue spawning monsters while the wave duration is not reached or until all spawns are completed while (Time.time - waveStartTime < currentWave.waveDuration && remainingSpawns.Exists(spawnCount => spawnCount > 0) && gameRunning) { if (!isPaused) { float currentTime = Time.time; for (int i = 0; i < currentWave.monsters.Count; i++) { // Check if it's time to spawn the next monster of this type and if there are remaining spawns if (currentTime >= nextSpawnTimes[i] && remainingSpawns[i] > 0) { SpawnMonster(currentWave.monsters[i].monsterPrefab, currentWave.monsters[i].goldPerMonster, currentWave.monsters[i].xpPerMonster); // Update the next spawn time for this monster type nextSpawnTimes[i] = currentTime + currentWave.monsters[i].spawnInterval; remainingSpawns[i]--; // Decrease the count of remaining spawns } } } yield return null; // Yield to keep the game responsive and check the next frame } // Mark this wave as completed currentWaveIndex++; // If there's another wave, start the next wave if (currentWaveIndex < waves.Count && gameRunning) { StopCoroutine(waveSpawner); // Ensure that the previous coroutine stops before starting a new one waveSpawner = StartCoroutine(SpawnWaves()); } else if (winCondition != WinCondition.SurvivalTime) // Only handle win condition here if it's not SurvivalTime { // If there are no more waves, ensure the spawning stops StopCoroutine(waveSpawner); waveSpawner = null; HandleWinCondition(); } } } /// /// Handles the win condition logic based on the selected game mode. /// private void HandleWinCondition() { if (winCondition == WinCondition.SurvivalTime) { EndGame(); } else if (winCondition == WinCondition.KillBoss) { // Spawn the boss immediately and show the final boss message SpawnMonster(bossPrefab, 0, 0); UIGameplay.Singleton.ShowFinalBossMessage(); // Show the final boss message StartCoroutine(CheckForBossDefeat()); } else if (winCondition == WinCondition.SurvivalTimeAndKillBoss) { // Stop spawning waves when time ends and spawn the boss if (waveSpawner != null) { StopCoroutine(waveSpawner); } if (timeRemaining <= 0) { SpawnMonster(bossPrefab, 0, 0); UIGameplay.Singleton.ShowFinalBossMessage(); // Show the final boss message StartCoroutine(CheckForBossDefeat()); } } } /// /// Coroutine that checks if the boss has been defeated. /// /// An IEnumerator for coroutine execution. private IEnumerator CheckForBossDefeat() { // Continuously check for any boss entity with isFinalBoss == true while (true) { var bossInstances = FindObjectsOfType(); bool bossDefeated = true; foreach (var boss in bossInstances) { if (boss.isFinalBoss && boss.gameObject.activeSelf) { bossDefeated = false; break; } } if (bossDefeated) { // When the final boss is defeated, end the game EndGame(); yield break; } yield return null; // Wait for the next frame and recheck } } void SpawnMonster(MonsterEntity monsterPrefab, int gold, int xp) { MonsterEntity monsterInstance = null; if (usePooling && isPoolingReady) { // Get an inactive mob from the pool if (mobPools.ContainsKey(monsterPrefab) && mobPools[monsterPrefab].Count > 0) { monsterInstance = mobPools[monsterPrefab].Dequeue(); monsterInstance.transform.position = GetRandomSpawnPoint(); monsterInstance.gameObject.SetActive(true); } else { // Instantiate a new one if pool is empty monsterInstance = Instantiate(monsterPrefab, GetRandomSpawnPoint(), Quaternion.identity); } } else { // Instantiate normally monsterInstance = Instantiate(monsterPrefab, GetRandomSpawnPoint(), Quaternion.identity); } if (monsterInstance.isFinalBoss) { UIGameplay.Singleton.SetFinalBoss(monsterInstance); } monsterInstance.SetMonsterGoldValue(gold); monsterInstance.SetMonsterXPValue(xp); } /// /// Gets a random spawn point from the available spawn points and returns a random position within that point's radius. /// /// A random Vector3 position from a random spawn point in the list. Vector3 GetRandomSpawnPoint() { if (spawnPointsList == null || spawnPointsList.Count == 0) { Debug.LogWarning("No spawn points available."); return Vector3.zero; } // Select a random spawn point from the list int randomIndex = Random.Range(0, spawnPointsList.Count); SpawnPoints selectedSpawnPoint = spawnPointsList[randomIndex]; // Get a random position from the selected spawn point return selectedSpawnPoint.GetRandomSpawnPoint(); } public void EndGame() { gameRunning = false; PauseGame(); GameManager.Singleton.EndGame(true); } public string GetFormattedTime() { if (timeDisplayFormat == TimeFormat.Seconds) { return Mathf.CeilToInt(timeRemaining).ToString() + "s"; } else if (timeDisplayFormat == TimeFormat.MinutesSeconds) { int minutes = Mathf.FloorToInt(timeRemaining / 60); int seconds = Mathf.FloorToInt(timeRemaining % 60); return string.Format("{0:00}:{1:00}", minutes, seconds); } return timeRemaining.ToString(); } /// /// Toggles the pause state of the game, stopping or resuming waves accordingly. /// public void TogglePause() { isPaused = !isPaused; // The SpawnWaves coroutine handles the pause internally, so no need to stop/restart it here. } /// /// Explicitly pauses the game, intended to be used for direct calls when pausing is needed without toggling. /// public void PauseGame() { if (!isPaused) // Only pause if not already paused. { isPaused = true; // The SpawnWaves coroutine will automatically respect the isPaused state. } } /// /// Explicitly resumes the game, intended to be used for direct calls when resuming is needed without toggling. /// public void ResumeGame() { if (isPaused) // Only resume if the game is currently paused. { isPaused = false; // The SpawnWaves coroutine will automatically resume respecting the isPaused state. } } /// /// Checks if the game is currently paused. /// /// True if the game is paused; otherwise, false. public bool IsPaused() { return isPaused; } public int GetMonstersKilled() { return monstersKilled; } public void IncrementMonstersKilled() { monstersKilled++; } public int GetGainGold() { return goldGain; } public void IncrementGainGold(int amount) { goldGain += amount; } public int GetGainXP() { return xpGain; } public void IncrementGainXP(int amount) { xpGain += amount; } /// /// Retrieves a list of available perks, filtering out any skill levels marked as evolved that the player cannot access yet. /// /// A list of perks available for the player to choose from. public List GetRandomPerks() { List availablePerks = new List(); foreach (var skill in character.GetCharacterData().skills) { int currentLevel = GetBaseSkillLevel(skill); int maxLevelIndex = skill.skillLevels.Count - 1; int nextLevel = currentLevel + 1; if (nextLevel <= maxLevelIndex) { bool isNextLevelEvolved = skill.skillLevels[nextLevel].isEvolved; bool hasRequiredStatPerk = skill.requireStatForEvolve == null || character.HasStatPerk(skill.requireStatForEvolve); if (!isNextLevelEvolved || hasRequiredStatPerk) { availablePerks.Add(skill); } } } if (character.GetCharacterData().autoAttack != null) { SkillData aaSkill = character.GetCharacterData().autoAttack; int currentLevel = GetBaseSkillLevel(aaSkill); int maxLevelIndex = aaSkill.skillLevels.Count - 1; int nextLevel = currentLevel + 1; if (nextLevel <= maxLevelIndex) { bool isNextLevelEvolved = aaSkill.skillLevels[nextLevel].isEvolved; bool hasRequiredStatPerk = aaSkill.requireStatForEvolve == null || character.HasStatPerk(aaSkill.requireStatForEvolve); if (!isNextLevelEvolved || hasRequiredStatPerk) { availablePerks.Add(aaSkill); } } } foreach (var skillPerk in skillPerkData) { int currentLevel = GetSkillLevel(skillPerk); int maxLevel = skillPerk.maxLevel; if (currentLevel < maxLevel) { // SkillPerk can be leveled up availablePerks.Add(skillPerk); } else if (currentLevel == maxLevel) { if (skillPerk.hasEvolution && character.HasStatPerk(skillPerk.perkRequireToEvolveSkill)) { // SkillPerk can be evolved availablePerks.Add(skillPerk); } } } foreach (var statPerk in statPerkData) { int currentLevel = GetPerkLevel(statPerk); if (currentLevel < maxLevelStatPerks) { availablePerks.Add(statPerk); } } ShufflePerks(availablePerks); return availablePerks.GetRange(0, Mathf.Min(3, availablePerks.Count)); } private void ShufflePerks(List perks) { for (int i = perks.Count - 1; i > 0; i--) { int j = Random.Range(0, i + 1); object temp = perks[i]; perks[i] = perks[j]; perks[j] = temp; } } /// /// Gets the current level of a given perk. /// /// The perk whose level is to be retrieved. /// The current level of the perk. Returns 0 if the perk is not found. public int GetPerkLevel(object perk) { if (perk is SkillPerkData skillPerk) { if (skillLevels.TryGetValue(skillPerk, out int level)) { return level; } } else if (perk is StatPerkData statPerk) { if (perkLevels.TryGetValue(statPerk, out int level)) { return level; } } return 0; } /// /// Sets the level of a given perk, handling skill and stat perks separately. /// /// The perk whose level is to be set. /// The new level of the perk. public void SetPerkLevel(object perk) { if (perk is SkillPerkData skillPerk) { LevelUpSkill(skillPerk); } else if (perk is StatPerkData statPerk) { if (perkLevels.ContainsKey(statPerk)) { perkLevels[statPerk]++; } else { perkLevels.Add(statPerk, 1); } } else { Debug.LogError("Unsupported perk type."); } } /// /// Levels up a skill to a specified level, including checks for evolution requirements. /// /// The skill perk to level up. /// The new level to set, if requirements are met. private void LevelUpSkill(SkillPerkData skillPerk) { if (skillPerk == null) { Debug.LogWarning("SkillPerkData is null."); return; } if (!skillLevels.ContainsKey(skillPerk)) { skillLevels[skillPerk] = 0; } int currentLevel = skillLevels[skillPerk]; if (currentLevel >= skillPerk.maxLevel) { Debug.LogWarning($"Attempting to level up {skillPerk.name} beyond max level {skillPerk.maxLevel}."); return; } if (currentLevel == skillPerk.maxLevel - 1 && skillPerk.hasEvolution && !character.HasStatPerk(skillPerk.perkRequireToEvolveSkill)) { Debug.LogWarning($"Cannot level up {skillPerk.name} to max level without having {skillPerk.perkRequireToEvolveSkill.name}."); return; } skillLevels[skillPerk] = currentLevel + 1; } /// /// Gets the current level of a specific skill. /// /// The skill perk to check the level for. /// The current level of the skill. public int GetSkillLevel(SkillPerkData skillPerk) { if (skillPerk == null) { return 0; } if (skillLevels.ContainsKey(skillPerk)) { return skillLevels[skillPerk]; } return 0; } /// /// Levels up a base skill of the character. /// /// The skill to level up. public void LevelUpBaseSkill(SkillData skill) { if (!skillBaseLevels.ContainsKey(skill)) { skillBaseLevels[skill] = 0; } int currentLevel = skillBaseLevels[skill]; int maxLevelIndex = skill.skillLevels.Count - 1; if (currentLevel <= maxLevelIndex) { skillBaseLevels[skill]++; Debug.Log($"Skill {skill.skillName} leveled up to {skillBaseLevels[skill]}."); } } /// /// Gets the current level of a base skill. /// /// The skill to check. /// The current level of the skill. public int GetBaseSkillLevel(SkillData skill) { if (skillBaseLevels.TryGetValue(skill, out int level)) { return level; } return 0; } } }