using OctoberStudio.Bossfight; using OctoberStudio.Extensions; using OctoberStudio.Pool; using OctoberStudio.Timeline; using OctoberStudio.UI; using System.Collections.Generic; using Unity.Collections; using UnityEngine; using UnityEngine.Events; using UnityEngine.Playables; namespace OctoberStudio { [DefaultExecutionOrder(-10)] public class EnemiesSpawner : MonoBehaviour { protected List enemies = new List(); [SerializeField] protected EnemiesDatabase database; [SerializeField] protected BossfightDatabase bossfightDatabase; [Space] [SerializeField] protected ScalingLabelBehavior enemiesDiedLabel; [Space] [Tooltip("Maximum amount of alive enemies at a time. No more enemies will be spawned until some of existing aren't defeated")] [SerializeField] protected int enemiesCap = 2000; [Header("Offscreen Teleport")] [Tooltip("When enabled, enemies that are behaind of the player will teleport to the front")] [SerializeField] protected bool isOffscreenTeleportEnabled = true; [Tooltip("Distance from the player to enemy, where 1 is a camera diagonal length")] [SerializeField] protected float diagonalDistanceMultiplier = 1.3f; [SerializeField, Range(0, 1f)] protected float teleportConeSize = 0.8f; [Tooltip("Enemy will stop teleporting if there are more than this amount of enemies")] [SerializeField] protected int enemiesTeleportCap = 100; protected int enemiesDiedCounter; protected Dictionary> enemyPools; protected Dictionary enemyDataDictionary; protected StageSave stageSave; public bool IsBossfightActive { get; set; } protected Camera mainCamera; protected virtual void Awake() { mainCamera = Camera.main; } // Were creating pools only for the enemies that are present in the Stage Timeline public virtual void Init(PlayableDirector director) { enemyDataDictionary = database.GetEnemyDataDictionary(); stageSave = GameController.SaveManager.GetSave("Stage"); Dictionary enemiesOnLevel = new Dictionary(); var waves = director.GetAssets(); for(int i = 0; i < waves.Count; i++) { var wave = waves[i]; var enemyType = wave.EnemyType; var enemiesCount = wave.EnemiesCount; if (enemiesOnLevel.ContainsKey(enemyType)) { if (enemiesOnLevel[enemyType] < enemiesCount) { enemiesOnLevel[enemyType] = enemiesCount; } } else { enemiesOnLevel.Add(enemyType, enemiesCount); } } var trackEnemies = new List(); foreach (var output in director.playableAsset.outputs) { if(output.sourceObject is WaveTrack waveTrack) { if (!trackEnemies.Contains(waveTrack.EnemyType)) { trackEnemies.Add(waveTrack.EnemyType); } } } enemyPools = new Dictionary>(); foreach(var enemyType in enemiesOnLevel.Keys) { var data = database.GetEnemyData(enemyType); var amount = enemiesOnLevel[enemyType]; if (amount > 100) amount = 100; if (amount < 0) amount = 1; var pool = new PoolComponent($"Enemy {enemyType}", data.Prefab, amount); enemyPools.Add(data.Type, pool); } foreach(var enemyType in trackEnemies) { if (!enemyPools.ContainsKey(enemyType)) { var data = database.GetEnemyData(enemyType); var pool = new PoolComponent($"Enemy {enemyType}", data.Prefab, 1); enemyPools.Add(data.Type, pool); } } enemiesDiedCounter = 0; if (!stageSave.ResetStageData) { enemiesDiedCounter = stageSave.EnemiesKilled; } enemiesDiedLabel.SetAmount(enemiesDiedCounter); } protected virtual void Update() { if (!isOffscreenTeleportEnabled || IsBossfightActive || enemies.Count > enemiesTeleportCap) return; var diagonalSqr = (CameraManager.HalfWidth * CameraManager.HalfWidth + CameraManager.HalfHeight * CameraManager.HalfHeight) * diagonalDistanceMultiplier; var diagonal = Mathf.Sqrt(diagonalSqr); var dotValue = teleportConeSize - 1; var modValue = Mathf.Clamp(enemies.Count / 20, 1, 100); int frame = Time.frameCount % modValue; for(int i = frame; i < enemies.Count; i += modValue) { var enemy = enemies[i]; if (enemy.WaveOverride != null && enemy.WaveOverride.DisableOffscreenTeleport) continue; var enemyToPlayer = enemy.transform.position - PlayerBehavior.Player.transform.position; var direction = enemyToPlayer.normalized; var dot = Vector2.Dot(direction, PlayerBehavior.Player.LookDirection); if(diagonalSqr < enemyToPlayer.sqrMagnitude && dot < dotValue) { var teleportPosition = PlayerBehavior.Player.transform.position + Quaternion.Euler(0, 0, Random.Range(-45, 45)) * PlayerBehavior.Player.LookDirection * diagonal; enemy.transform.position = teleportPosition; } } } public virtual EnemyBehavior GetClosestEnemy(Vector2 point) { EnemyBehavior closestEnemy = null; float closestDistance = float.MaxValue; for(int i = 0; i < enemies.Count; i++) { if (enemies[i] == null) { enemies.RemoveAt(i); i--; continue; } float distance = (point - enemies[i].transform.position.XY()).sqrMagnitude; if(distance < closestDistance) { closestEnemy = enemies[i]; closestDistance = distance; } } return closestEnemy; } public virtual EnemyBehavior Spawn(EnemyType enemyType, Vector2 position, UnityAction onEnemyDiedCallback = null) { if (enemies.Count >= enemiesCap) return null; var enemyData = enemyDataDictionary[enemyType]; if (!enemyPools.ContainsKey(enemyType)) { var pool = new PoolComponent($"Enemy {enemyType}", enemyData.Prefab, 10); enemyPools.Add(enemyData.Type, pool); } var enemy = enemyPools[enemyType].GetEntity(); enemy.SetData(enemyData); enemy.transform.position = position; enemy.onEnemyDied += OnEnemyDied; if (onEnemyDiedCallback != null) enemy.onEnemyDied += onEnemyDiedCallback; enemy.Play(); enemies.Add(enemy); return enemy; } public virtual void Spawn(EnemyType type, WaveOverride waveOverride, bool circularSpawn = false, int amount = 1, UnityAction onEnemyDiedCallback = null) { var cameraHeight = mainCamera.orthographicSize; var cameraWidth = cameraHeight * mainCamera.aspect; var cameraDiagonal = Mathf.Sqrt(cameraWidth * cameraWidth + cameraHeight * cameraHeight); for (int i = 0; i < amount; i++) { if (enemies.Count >= enemiesCap) return; var enemy = enemyPools[type].GetEntity(); enemy.SetData(enemyDataDictionary[type]); enemy.SetWaveOverride(waveOverride); var triesCount = 0; var maxTriesCount = 10; var position = Vector3.zero; var foundPosition = false; while(triesCount < maxTriesCount) { triesCount++; // when spawning a lot of enemies at once we want to offset them to not overload physics computations var additionalDistance = amount > 100 ? Mathf.Sqrt(enemies.Count) * 0.1f : 0; if (circularSpawn) { position = PlayerBehavior.Player.transform.position + Random.onUnitSphere.SetZ(0).normalized * (cameraDiagonal * 1.05f + Random.value * 0.2f + additionalDistance); } else { position = CameraManager.GetRandomPointOutsideCamera(0.5f + Random.value * 0.2f + additionalDistance); } if(StageController.FieldManager.ValidatePosition(position, Vector2.zero)) { foundPosition = true; break; } } if (!foundPosition) { for (int j = 1; j < 10; j++) { var middlePosition = Vector3.Lerp(position, PlayerBehavior.Player.transform.position, 1 - j / 10f); if (StageController.FieldManager.ValidatePosition(middlePosition, Vector2.zero)) { foundPosition = true; position = middlePosition; break; } } } if (!foundPosition) { position = StageController.FieldManager.GetRandomPositionOnBorder(); } enemy.transform.position = position; enemy.onEnemyDied += OnEnemyDied; if (onEnemyDiedCallback != null) enemy.onEnemyDied += onEnemyDiedCallback; enemy.Play(); enemies.Add(enemy); } } public virtual EnemyBehavior GetRandomVisibleEnemy() { if (enemies.Count == 0) return null; // Trying to find random visible enemy 10 times for(int i = 0; i < 10; i++) { var randomIndex = Random.Range(0, enemies.Count); var enemy = enemies[randomIndex]; if (enemy.IsVisible) return enemy; } for(int i = 0; i < enemies.Count; i++) { var enemy = enemies[i]; if (enemy.IsVisible) return enemy; } return null; } public virtual List GetEnemiesInRadius(Vector2 position, float radius) { var result = new List(); float radiusSqr = radius * radius; for(int i = 0; i < enemies.Count; i++) { if ((enemies[i].transform.position.XY() - position).sqrMagnitude <= radiusSqr) { result.Add(enemies[i]); } } return result; } public virtual void KillEveryEnemy() { foreach(var enemy in enemies) { enemy.onEnemyDied -= OnEnemyDied; enemy.Kill(); } enemiesDiedCounter += enemies.Count; stageSave.EnemiesKilled = enemiesDiedCounter; enemiesDiedLabel.SetAmount(enemiesDiedCounter); enemies.Clear(); } public virtual void DealDamageToAllEnemies(float damage) { var aliveEnemies = new List(); foreach(var enemy in enemies) { if(enemy.HP <= damage) { // if enemy is not a boss if(enemy.Data != null) { enemy.onEnemyDied -= OnEnemyDied; enemy.Kill(); foreach (var dropData in enemy.GetDropData()) { if (dropData.Chance == 0) continue; if (Random.value * 100 <= dropData.Chance && StageController.DropManager.CheckDropCooldown(dropData.DropType)) { StageController.DropManager.Drop(dropData.DropType, enemy.transform.position.XY() + Random.insideUnitCircle * 0.2f); } } } else { aliveEnemies.Add(enemy); } } else { // if enemy is not a boss if (enemy.Data != null) { enemy.TakeDamage(damage); } aliveEnemies.Add(enemy); } } enemiesDiedCounter += enemies.Count - aliveEnemies.Count; stageSave.EnemiesKilled = enemiesDiedCounter; enemiesDiedLabel.SetAmount(enemiesDiedCounter); enemies.Clear(); enemies.AddRange(aliveEnemies); } protected virtual void OnEnemyDied(EnemyBehavior enemy) { enemies.RemoveSwapBack(enemy); enemy.onEnemyDied -= OnEnemyDied; foreach(var dropData in enemy.GetDropData()) { if(dropData.Chance == 0) continue; if(Random.value * 100 <= dropData.Chance && StageController.DropManager.CheckDropCooldown(dropData.DropType)) { StageController.DropManager.Drop(dropData.DropType, enemy.transform.position.XY() + Random.insideUnitCircle * 0.2f); } } enemiesDiedCounter++; stageSave.EnemiesKilled = enemiesDiedCounter; enemiesDiedLabel.SetAmount(enemiesDiedCounter); } protected virtual void OnBossDied(EnemyBehavior boss) { enemies.RemoveSwapBack(boss); boss.onEnemyDied -= OnBossDied; if (boss.ShouldSpawnChestOnDeath && StageController.AbilityManager.HasAvailableAbilities()) StageController.DropManager.Drop(DropType.Chest, boss.transform.position.XY() + Random.insideUnitCircle); StageController.DropManager.Drop(DropType.Magnet, boss.transform.position.XY() + Random.insideUnitCircle); StageController.DropManager.Drop(DropType.Food, boss.transform.position.XY() + Random.insideUnitCircle); enemiesDiedCounter++; stageSave.EnemiesKilled = enemiesDiedCounter; enemiesDiedLabel.SetAmount(enemiesDiedCounter); } public virtual EnemyBehavior SpawnBoss(BossType bossType, Vector2 spawnPosition, UnityAction onBossDied = null) { var bossData = bossfightDatabase.GetBossfight(bossType); var boss = Instantiate(bossData.BossPrefab).GetComponent(); boss.transform.position = spawnPosition; boss.Play(); boss.onEnemyDied += OnBossDied; boss.onEnemyDied += onBossDied; enemies.Add(boss); return boss; } public virtual BossfightData GetBossData(BossType bossType) { return bossfightDatabase.GetBossfight(bossType); } } }