423 lines
14 KiB
C#
423 lines
14 KiB
C#
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<DRLevelPhase> _phaseBuffer = new List<DRLevelPhase>();
|
|
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId =
|
|
new Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>>();
|
|
|
|
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<DRLevelPhase> phases,
|
|
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> 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<DRLevelSpawnEntry> 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
|
|
}
|
|
}
|