using System.Collections.Generic; using System.Globalization; using GameFramework.Event; using GameFramework.Resource; using GeometryTD.CustomUtility; using GeometryTD.DataTable; using GeometryTD.Definition; using GeometryTD.Entity; using GeometryTD.Entity.EntityData; using UnityEngine; using UnityGameFramework.Runtime; namespace GeometryTD.CustomComponent { public class CombatScheduler { private enum SchedulerState : byte { Idle = 0, WaitingForMap = 1, RunningPhase = 2, Completed = 3, Failed = 4 } private readonly List _phaseBuffer = new List(); private readonly Dictionary> _spawnEntriesByPhaseId = new Dictionary>(); private readonly EnemyManager _enemyManager = new EnemyManager(); private EntityComponent _entity; private DRLevel _currentLevel; private DRLevelPhase _currentPhase; private int _currentPhaseIndex; private float _currentPhaseElapsed; private SchedulerState _state = SchedulerState.Idle; private int _loadingMapEntityId; private int _loadedMapEntityId; private bool _initialized; private bool _isEntityEventSubscribed; private MapEntity _currentMap; public bool IsRunning => _state == SchedulerState.WaitingForMap || _state == SchedulerState.RunningPhase; public bool IsCompleted => _state == SchedulerState.Completed; public DRLevel CurrentLevel => _currentLevel; public DRLevelPhase CurrentPhase => _currentPhase; public MapEntity CurrentMap => _currentMap; public void OnInit() { if (!_initialized) { _entity = GameEntry.Entity; EnsureEntityEventSubscribed(); _enemyManager.OnInit(this); _initialized = true; } ResetRuntime(); } public void Start( DRLevel level, IReadOnlyList phases, IReadOnlyDictionary> spawnEntriesByPhaseId) { if (!_initialized || _entity == null) { _state = SchedulerState.Failed; Log.Warning("CombatScheduler start failed. Runtime is not initialized."); return; } if (level == null || phases == null || phases.Count <= 0) { _state = SchedulerState.Failed; Log.Warning("CombatScheduler start failed. Invalid level or phase data."); return; } _enemyManager.EndPhase(); HideCurrentMapIfNeeded(); ResetRuntime(); _currentLevel = level; foreach (var phase in phases) { _phaseBuffer.Add(phase); } if (spawnEntriesByPhaseId != null) { foreach (var pair in spawnEntriesByPhaseId) { _spawnEntriesByPhaseId[pair.Key] = pair.Value; } } if (_phaseBuffer.Count <= 0) { _state = SchedulerState.Failed; Log.Warning("CombatScheduler start failed. Level '{0}' has no phase data.", level.Id); return; } if (!TryShowMap(level)) { _state = SchedulerState.Failed; return; } _currentPhase = null; _currentPhaseIndex = -1; _currentPhaseElapsed = 0f; _state = SchedulerState.WaitingForMap; Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, _phaseBuffer.Count); } public void OnUpdate(float elapseSeconds, float realElapseSeconds) { switch (_state) { case SchedulerState.WaitingForMap: if (_currentMap == null) { return; } BeginNextPhase(); return; case SchedulerState.RunningPhase: UpdateCurrentPhase(elapseSeconds, realElapseSeconds); return; default: return; } } public void OnDestroy() { if (!_initialized) { return; } _enemyManager.OnDestroy(); HideCurrentMapIfNeeded(); ResetRuntime(); UnsubscribeEntityEvents(); _entity = null; _initialized = false; } private bool TryShowMap(DRLevel level) { string mapAssetName = level.Id.ToString(); string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName); if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist) { Log.Warning( "CombatScheduler start failed. Level '{0}' map asset not found: '{1}'.", level.Id, mapAssetPath); return false; } _loadingMapEntityId = _entity.GenerateSerialId(); _entity.ShowMap(new MapData(_loadingMapEntityId, level.Id, Vector3.zero), mapAssetName); Log.Info( "CombatScheduler loading map. Level={0}, Asset='{1}', EntityId={2}.", level.Id, mapAssetPath, _loadingMapEntityId); return true; } private void UpdateCurrentPhase(float elapseSeconds, float realElapseSeconds) { if (_currentPhase == null) { _state = SchedulerState.Failed; Log.Warning("CombatScheduler update failed. Current phase is null."); return; } _currentPhaseElapsed += elapseSeconds; _enemyManager.OnUpdate(elapseSeconds, realElapseSeconds); if (!ShouldEndCurrentPhase()) { return; } CompleteCurrentPhase(); } private bool ShouldEndCurrentPhase() { switch (_currentPhase.EndType) { case PhaseEndType.TimeElapsed: return _currentPhaseElapsed >= ResolveTimeElapsedThreshold(_currentPhase); case PhaseEndType.EnemiesCleared: case PhaseEndType.BossDead: return _enemyManager.IsPhaseSpawnCompleted && _enemyManager.AliveEnemyCount <= 0; case PhaseEndType.None: default: if (_currentPhase.DurationSeconds > 0 && _currentPhaseElapsed >= _currentPhase.DurationSeconds) { return true; } return _enemyManager.IsPhaseSpawnCompleted && _enemyManager.AliveEnemyCount <= 0; } } private float ResolveTimeElapsedThreshold(DRLevelPhase phase) { if (!string.IsNullOrWhiteSpace(phase.EndParam) && float.TryParse(phase.EndParam, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedSeconds)) { return parsedSeconds; } if (phase.DurationSeconds > 0) { return phase.DurationSeconds; } return 0f; } private void CompleteCurrentPhase() { _enemyManager.EndPhase(); Log.Info( "CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.", _currentLevel != null ? _currentLevel.Id : 0, _currentPhase != null ? _currentPhase.Id : 0, _currentPhaseElapsed); BeginNextPhase(); } private void BeginNextPhase() { _currentPhaseIndex++; if (_currentPhaseIndex >= _phaseBuffer.Count) { _state = SchedulerState.Completed; _currentPhase = null; Log.Info("CombatScheduler level completed. Level={0}.", _currentLevel != null ? _currentLevel.Id : 0); return; } _currentPhase = _phaseBuffer[_currentPhaseIndex]; _currentPhaseElapsed = 0f; IReadOnlyList spawnEntries = null; _spawnEntriesByPhaseId.TryGetValue(_currentPhase.Id, out spawnEntries); _enemyManager.BeginPhase(_currentPhase, spawnEntries); _state = SchedulerState.RunningPhase; Log.Info( "CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.", _currentLevel != null ? _currentLevel.Id : 0, _currentPhase.Id, _currentPhase.EndType, spawnEntries != null ? spawnEntries.Count : 0); } private void HideCurrentMapIfNeeded() { if (_entity == null) { _currentMap = null; _loadingMapEntityId = 0; _loadedMapEntityId = 0; return; } if (_loadingMapEntityId != 0) { EntityBase loadingMap = _entity.GetGameEntity(_loadingMapEntityId); if (loadingMap != null) { _entity.HideEntity(loadingMap); } } if (_loadedMapEntityId != 0) { EntityBase loadedMap = _entity.GetGameEntity(_loadedMapEntityId); if (loadedMap != null) { _entity.HideEntity(loadedMap); } } _loadingMapEntityId = 0; _loadedMapEntityId = 0; _currentMap = null; } private void EnsureEntityEventSubscribed() { if (_isEntityEventSubscribed) { return; } GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure); GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); _isEntityEventSubscribed = true; } private void UnsubscribeEntityEvents() { if (!_isEntityEventSubscribed) { return; } GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure); GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); _isEntityEventSubscribed = false; } private void ResetRuntime() { _phaseBuffer.Clear(); _spawnEntriesByPhaseId.Clear(); _currentLevel = null; _currentPhase = null; _currentPhaseIndex = -1; _currentPhaseElapsed = 0f; _state = SchedulerState.Idle; } #region Event Handlers private void OnShowEntitySuccess(object sender, GameEventArgs e) { if (!(e is ShowEntitySuccessEventArgs ne)) { return; } if (ne.EntityLogicType != typeof(MapEntity) || ne.Entity.Id != _loadingMapEntityId) { return; } _loadedMapEntityId = _loadingMapEntityId; _loadingMapEntityId = 0; _currentMap = ne.Entity.Logic as MapEntity; if (_currentMap == null) { _state = SchedulerState.Failed; Log.Error("Loaded map entity logic is invalid. EntityId={0}.", ne.Entity.Id); return; } Log.Info( "Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.", _currentLevel != null ? _currentLevel.Id : 0, _currentMap.PathCells.Count, _currentMap.FoundationCells.Count, _currentMap.Spawners.Length, _currentMap.House != null ? _currentMap.House.name : "None"); } private void OnShowEntityFailure(object sender, GameEventArgs e) { if (!(e is ShowEntityFailureEventArgs ne)) { return; } if (ne.EntityLogicType != typeof(MapEntity) || ne.EntityId != _loadingMapEntityId) { return; } _loadingMapEntityId = 0; _currentMap = null; _state = SchedulerState.Failed; Log.Error( "Map load failed. LevelId={0}, Asset='{1}', Error='{2}'.", _currentLevel != null ? _currentLevel.Id : 0, ne.EntityAssetName, ne.ErrorMessage); } private void OnHideEntityComplete(object sender, GameEventArgs e) { if (!(e is HideEntityCompleteEventArgs ne)) { return; } if (ne.EntityId == _loadedMapEntityId) { _loadedMapEntityId = 0; _currentMap = null; } if (ne.EntityId == _loadingMapEntityId) { _loadingMapEntityId = 0; } } #endregion } }