using System.Collections.Generic; using GameFramework.DataTable; using GameFramework.Event; using GeometryTD.DataTable; using GeometryTD.Definition; using GeometryTD.Entity; using GeometryTD.Entity.EntityData; using GeometryTD.Map; using UnityEngine; using UnityGameFramework.Runtime; namespace GeometryTD.CustomComponent { public class EnemyManager { private sealed class SpawnEntryRuntime { public DRLevelSpawnEntry Entry; public bool Completed; public float NextTriggerTime; public float EndTime; public int RemainingCount; } private const int DefaultEnemyConfigId = 1; private const float MinStreamInterval = 0.05f; private const float MinBurstGap = 0.01f; private readonly List _spawners = new List(); private readonly Dictionary _spawnerByOrder = new Dictionary(); private readonly List _pathBuffer = new List(); private readonly List _spawnRuntimes = new List(); private readonly HashSet _trackedEnemyEntityIds = new HashSet(); private readonly List _trackedEnemyIdBuffer = new List(); private readonly Dictionary _trackedEnemyConfigByEntityId = new Dictionary(); private CombatScheduler _combatScheduler; private EntityComponent _entity; private IDataTable _drEnemy; private int _spawnEnemyMaxCount = 5000; private int _currentEnemyCount; private int _defeatedEnemyCount; private int _nextSpawnerIndex; private int _currentMapEntityId; private bool _initialized; private bool _enemyConfigMissingLogged; private float _phaseElapsed; private bool _isPhaseRunning; public int AliveEnemyCount => _currentEnemyCount; public int DefeatedEnemyCount => _defeatedEnemyCount; public bool IsPhaseSpawnCompleted { get; private set; } = true; public bool IsPhaseRunning => _isPhaseRunning; public void OnInit(CombatScheduler combatScheduler) { _combatScheduler = combatScheduler; if (_initialized) { return; } _entity = GameEntry.Entity; _drEnemy = GameEntry.DataTable.GetDataTable(); _currentEnemyCount = 0; _defeatedEnemyCount = 0; _nextSpawnerIndex = 0; _currentMapEntityId = 0; _enemyConfigMissingLogged = false; _spawners.Clear(); _spawnerByOrder.Clear(); _pathBuffer.Clear(); _spawnRuntimes.Clear(); _trackedEnemyEntityIds.Clear(); _trackedEnemyIdBuffer.Clear(); _trackedEnemyConfigByEntityId.Clear(); _phaseElapsed = 0f; _isPhaseRunning = false; IsPhaseSpawnCompleted = true; GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure); GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); _initialized = true; } public void BeginPhase(DRLevelPhase phase, IReadOnlyList spawnEntries) { if (!_initialized || _combatScheduler == null) { return; } _ = phase; EndPhase(); _phaseElapsed = 0f; _isPhaseRunning = true; IsPhaseSpawnCompleted = false; RefreshSpawnerCache(true); if (spawnEntries != null) { for (int i = 0; i < spawnEntries.Count; i++) { SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]); if (runtime != null) { _spawnRuntimes.Add(runtime); } } } IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0; } public void OnUpdate(float elapseSeconds, float realElapseSeconds) { if (!_initialized || _combatScheduler == null || !_isPhaseRunning) { return; } RefreshSpawnerCache(false); _phaseElapsed += elapseSeconds; UpdateSpawnRuntimes(); } public void EndPhase() { _isPhaseRunning = false; _phaseElapsed = 0f; _spawnRuntimes.Clear(); IsPhaseSpawnCompleted = true; } public void OnDestroy() { if (!_initialized) { _combatScheduler = null; return; } CleanupTrackedEnemies(); EndPhase(); GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure); GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); _spawners.Clear(); _spawnerByOrder.Clear(); _pathBuffer.Clear(); _trackedEnemyEntityIds.Clear(); _trackedEnemyIdBuffer.Clear(); _trackedEnemyConfigByEntityId.Clear(); _currentEnemyCount = 0; _defeatedEnemyCount = 0; _currentMapEntityId = 0; _nextSpawnerIndex = 0; _combatScheduler = null; _initialized = false; } public void ResetCombatStats() { _defeatedEnemyCount = 0; } public void CleanupTrackedEnemies() { if (_trackedEnemyEntityIds.Count <= 0) { _currentEnemyCount = 0; return; } _trackedEnemyIdBuffer.Clear(); foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds) { _trackedEnemyIdBuffer.Add(trackedEnemyEntityId); } _trackedEnemyEntityIds.Clear(); _trackedEnemyConfigByEntityId.Clear(); _currentEnemyCount = 0; if (_entity == null) { return; } for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++) { int trackedEnemyEntityId = _trackedEnemyIdBuffer[i]; if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId)) { _entity.HideEntity(trackedEnemyEntityId); } } } private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry) { if (entry == null || entry.EntryType == EntryType.None) { return null; } SpawnEntryRuntime runtime = new SpawnEntryRuntime { Entry = entry, Completed = false, NextTriggerTime = Mathf.Max(0f, entry.StartTime), EndTime = Mathf.Max(0f, entry.StartTime), RemainingCount = Mathf.Max(0, entry.Count) }; switch (entry.EntryType) { case EntryType.Stream: { float duration = Mathf.Max(0f, entry.Duration); runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime; runtime.Completed = entry.Count <= 0; return runtime; } case EntryType.Burst: case EntryType.Boss: runtime.Completed = runtime.RemainingCount <= 0; return runtime; default: return null; } } private void UpdateSpawnRuntimes() { bool allCompleted = true; for (int i = 0; i < _spawnRuntimes.Count; i++) { SpawnEntryRuntime runtime = _spawnRuntimes[i]; if (runtime.Completed) { continue; } switch (runtime.Entry.EntryType) { case EntryType.Stream: ProcessStreamRuntime(runtime); break; case EntryType.Burst: case EntryType.Boss: ProcessBurstRuntime(runtime); break; default: runtime.Completed = true; break; } if (!runtime.Completed) { allCompleted = false; } } IsPhaseSpawnCompleted = allCompleted; } private void ProcessStreamRuntime(SpawnEntryRuntime runtime) { if (_phaseElapsed < runtime.NextTriggerTime) { return; } int countPerWave = Mathf.Max(0, runtime.Entry.Count); if (countPerWave <= 0) { runtime.Completed = true; return; } float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval; while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime) { SpawnEnemies(runtime.Entry, countPerWave); runtime.NextTriggerTime += interval; } if (runtime.NextTriggerTime > runtime.EndTime) { runtime.Completed = true; } } private void ProcessBurstRuntime(SpawnEntryRuntime runtime) { if (_phaseElapsed < runtime.NextTriggerTime) { return; } if (runtime.RemainingCount <= 0) { runtime.Completed = true; return; } float gap = runtime.Entry.Gap; if (gap <= 0f) { SpawnEnemies(runtime.Entry, runtime.RemainingCount); runtime.RemainingCount = 0; runtime.Completed = true; return; } gap = Mathf.Max(gap, MinBurstGap); while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0) { SpawnEnemies(runtime.Entry, 1); runtime.RemainingCount--; runtime.NextTriggerTime += gap; } runtime.Completed = runtime.RemainingCount <= 0; } private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount) { if (spawnCount <= 0) { return; } Spawner spawner = ResolveSpawner(entry.SpawnPointId); if (spawner == null) { return; } MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null; if (currentMap == null || !currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) || _pathBuffer.Count <= 0) { return; } DREnemy enemyConfig = GetEnemyConfig(entry.EnemyId); if (enemyConfig == null) { return; } for (int i = 0; i < spawnCount; i++) { if (_currentEnemyCount >= _spawnEnemyMaxCount) { break; } int enemyEntityId = _entity.GenerateSerialId(); _trackedEnemyEntityIds.Add(enemyEntityId); _trackedEnemyConfigByEntityId[enemyEntityId] = enemyConfig; EnemyData enemyData = new EnemyData( enemyEntityId, enemyConfig.EntityId, _pathBuffer[0], enemyConfig.BaseHp, enemyConfig.Speed, _pathBuffer); _entity.ShowEnemy(enemyData); } } private DREnemy GetEnemyConfig(int enemyId) { if (_drEnemy == null) { _drEnemy = GameEntry.DataTable.GetDataTable(); if (_drEnemy == null) { if (!_enemyConfigMissingLogged) { Log.Warning("EnemyManagerComponent can not find DREnemy data table."); _enemyConfigMissingLogged = true; } return null; } } if (enemyId > 0) { DREnemy targetConfig = _drEnemy.GetDataRow(enemyId); if (targetConfig != null) { return targetConfig; } } DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId); if (defaultConfig != null) { return defaultConfig; } DREnemy[] allConfigs = _drEnemy.GetAllDataRows(); if (allConfigs.Length > 0) { return allConfigs[0]; } if (!_enemyConfigMissingLogged) { Log.Warning("EnemyManagerComponent found no enemy configs."); _enemyConfigMissingLogged = true; } return null; } private Spawner ResolveSpawner(int spawnPointId) { if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner)) { return mappedSpawner; } if (_spawners.Count <= 0) { return null; } Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count]; _nextSpawnerIndex++; return fallbackSpawner; } private void RefreshSpawnerCache(bool force) { MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null; if (currentMap == null) { _spawners.Clear(); _spawnerByOrder.Clear(); _currentMapEntityId = 0; _nextSpawnerIndex = 0; return; } if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0) { return; } _spawners.Clear(); _spawnerByOrder.Clear(); _nextSpawnerIndex = 0; _currentMapEntityId = currentMap.Id; Spawner[] mapSpawners = currentMap.Spawners; for (int i = 0; i < mapSpawners.Length; i++) { Spawner spawner = mapSpawners[i]; if (spawner == null) { continue; } if (!currentMap.TryGetDefaultPathCells(spawner, out _)) { continue; } _spawners.Add(spawner); if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder)) { _spawnerByOrder[spawner.SpawnOrder] = spawner; } } _spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder)); } private void OnShowEntitySuccess(object sender, GameEventArgs e) { if (!(e is ShowEntitySuccessEventArgs ne)) return; if (ne.EntityLogicType == typeof(EnemyEntity) && _trackedEnemyEntityIds.Contains(ne.Entity.Id)) { _currentEnemyCount++; } } private void OnShowEntityFailure(object sender, GameEventArgs e) { if (!(e is ShowEntityFailureEventArgs ne)) { return; } if (ne.EntityLogicType != typeof(EnemyEntity)) { return; } _trackedEnemyEntityIds.Remove(ne.EntityId); _trackedEnemyConfigByEntityId.Remove(ne.EntityId); } private void OnHideEntityComplete(object sender, GameEventArgs e) { if (!(e is HideEntityCompleteEventArgs ne)) { return; } if (!_trackedEnemyEntityIds.Remove(ne.EntityId)) { _trackedEnemyConfigByEntityId.Remove(ne.EntityId); return; } _currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1); bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId); bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning; int baseDamage = 0; int droppedCoin = 0; int droppedGold = 0; if (_trackedEnemyConfigByEntityId.TryGetValue(ne.EntityId, out DREnemy enemyConfig) && enemyConfig != null) { baseDamage = Mathf.Max(0, enemyConfig.BaseDamage); if (wasKilled) { droppedCoin = Mathf.Max(0, enemyConfig.DropCoin); float dropRate = enemyConfig.DropPercent > 1f ? Mathf.Clamp01(enemyConfig.DropPercent * 0.01f) : Mathf.Clamp01(enemyConfig.DropPercent); if (enemyConfig.DropGold > 0 && dropRate > 0f && Random.value <= dropRate) { droppedGold = Mathf.Max(0, enemyConfig.DropGold); } } } if (isCombatRunning && wasKilled) { _defeatedEnemyCount++; _combatScheduler.OnEnemyDefeatedRewardResolved(droppedCoin, droppedGold); } else if (isCombatRunning && baseDamage > 0) { _combatScheduler.OnEnemyReachedBase(baseDamage); } _trackedEnemyConfigByEntityId.Remove(ne.EntityId); } } }