geometry-tower-defense/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs

483 lines
16 KiB
C#

using System.Collections.Generic;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public class CombatScheduler
{
private enum SchedulerState : byte
{
Idle = 0,
WaitingForLoading = 1,
RunningPhase = 2,
WaitingForFinishReturn = 3,
Completed = 4,
Failed = 5
}
private readonly List<DRLevelPhase> _phaseBuffer = new();
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
private readonly EnemyManager _enemyManager = new();
private readonly PhaseLoopRuntime _phaseLoopRuntime = new();
private readonly CombatLoadSession _loadSession = new();
private readonly CombatEventBridge _eventBridge = new();
private readonly CombatResourceManager _combatResourceManager = new();
private EntityComponent _entity;
private DRLevel _currentLevel;
private CombatFinishFormUseCase _combatFinishFormUseCase;
private SchedulerState _state = SchedulerState.Idle;
private bool _initialized;
private bool _isFinishAsVictory = true;
public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase;
public bool IsCompleted => _state == SchedulerState.Completed;
public DRLevel CurrentLevel => _currentLevel;
public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase;
public MapEntity CurrentMap => _loadSession.CurrentMap;
public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex;
public int PhaseCount => _phaseLoopRuntime.PhaseCount;
public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat;
public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount;
public int GainedCoin => _combatResourceManager.GainedCoin;
public int GainedGold => _combatResourceManager.GainedGold;
public void OnInit()
{
if (!_initialized)
{
_entity = GameEntry.Entity;
_eventBridge.Bind(
OnShowEntitySuccess,
OnShowEntityFailure,
OnHideEntityComplete,
OnOpenUIFormSuccess,
OnOpenUIFormFailure,
OnCloseUIFormComplete);
_enemyManager.OnInit(this);
_loadSession.OnInit(_entity);
EnsureCombatFinishFormUseCaseBound();
_initialized = true;
}
ResetRuntime();
}
public bool Start(
DRLevel level,
IReadOnlyList<DRLevelPhase> phases,
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId)
{
if (!_initialized || _entity == null)
{
return HandleStartFailure("CombatScheduler start failed. Runtime is not initialized.");
}
if (level == null || phases == null || phases.Count <= 0)
{
return HandleStartFailure("CombatScheduler start failed. Invalid level or phase data.");
}
CleanupAllCombatEntities();
CloseCombatFinishForm();
_enemyManager.EndPhase();
_enemyManager.ResetCombatStats();
ResetRuntime();
_combatResourceManager.Reset();
_isFinishAsVictory = true;
_currentLevel = level;
for (int i = 0; i < phases.Count; i++)
{
DRLevelPhase phase = phases[i];
if (phase != null)
{
_phaseBuffer.Add(phase);
}
}
if (spawnEntriesByPhaseId != null)
{
foreach (var pair in spawnEntriesByPhaseId)
{
_spawnEntriesByPhaseId[pair.Key] = pair.Value;
}
}
_phaseLoopRuntime.SetPhases(_phaseBuffer);
if (_phaseLoopRuntime.PhaseCount <= 0)
{
return HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data.");
}
if (!_loadSession.StartLoading(level, out string loadError))
{
return HandleStartFailure($"CombatScheduler start failed. {loadError}");
}
_state = SchedulerState.WaitingForLoading;
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id,
_phaseLoopRuntime.PhaseCount);
return true;
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
switch (_state)
{
case SchedulerState.WaitingForLoading:
if (!_loadSession.IsReady)
{
return;
}
BeginNextPhase();
return;
case SchedulerState.RunningPhase:
UpdateCurrentPhase(elapseSeconds, realElapseSeconds);
return;
default:
return;
}
}
public void OnDestroy()
{
if (!_initialized)
{
return;
}
CleanupAllCombatEntities();
CloseCombatFinishForm();
_enemyManager.OnDestroy();
ResetRuntime();
_eventBridge.Unbind();
_combatFinishFormUseCase = null;
_entity = null;
_initialized = false;
}
public bool TryEndCombatByPlayer()
{
if (_state != SchedulerState.RunningPhase)
{
return false;
}
if (!_phaseLoopRuntime.TryRequestEndCombat())
{
return false;
}
EnterFinishFlow("Combat ended by player.", true);
return true;
}
private void UpdateCurrentPhase(float elapseSeconds, float realElapseSeconds)
{
DRLevelPhase currentPhase = _phaseLoopRuntime.CurrentPhase;
if (currentPhase == null)
{
EnterFailureFallback("CombatScheduler update failed. Current phase is null.");
return;
}
_phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
_enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted,
_enemyManager.AliveEnemyCount))
{
return;
}
CompleteCurrentPhase();
}
private void CompleteCurrentPhase()
{
_enemyManager.EndPhase();
Log.Info(
"CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.",
_currentLevel != null ? _currentLevel.Id : 0,
_phaseLoopRuntime.CurrentPhase != null ? _phaseLoopRuntime.CurrentPhase.Id : 0,
_phaseLoopRuntime.CurrentPhaseElapsed);
BeginNextPhase();
}
private void BeginNextPhase()
{
if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
{
EnterFinishFlow("Combat ended after loop completion.", true);
return;
}
_spawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries);
_enemyManager.BeginPhase(nextPhase, spawnEntries);
_state = SchedulerState.RunningPhase;
GameEntry.Event.Fire(
this,
CombatProcessEventArgs.Create(_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.PhaseCount));
GameEntry.Event.Fire(
this,
CombatEnemyHpRateChangedEventArgs.Create(
ResolveEnemyHpRateMultiplier(_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.PhaseCount)));
Log.Info(
"CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.",
_currentLevel != null ? _currentLevel.Id : 0,
_phaseLoopRuntime.DisplayPhaseIndex,
nextPhase.EndType,
spawnEntries != null ? spawnEntries.Count : 0);
}
private void EnterFinishFlow(string reason, bool isVictory)
{
int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount;
BackpackInventoryData rewardInventory = _combatResourceManager.GetRewardInventorySnapshot();
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
// Step 1: stop runtime and clear enemy entities only.
_enemyManager.EndPhase();
_state = SchedulerState.WaitingForFinishReturn;
_isFinishAsVictory = isVictory;
_enemyManager.CleanupTrackedEnemies();
Log.Info(
"CombatScheduler entered finish flow. Level={0}. Reason={1}",
_currentLevel != null ? _currentLevel.Id : 0,
reason);
OpenCombatFinishForm(defeatedEnemyCount, _combatResourceManager.GainedGold, rewardInventory);
}
public void OnEnemyReachedBase(int baseDamage)
{
if (_state != SchedulerState.RunningPhase)
{
return;
}
int resolvedBaseDamage = Mathf.Max(0, baseDamage);
if (resolvedBaseDamage <= 0)
{
return;
}
CombatNodeComponent combatNode = GameEntry.CombatNode;
if (combatNode == null)
{
return;
}
int currentBaseHp = combatNode.ApplyBaseDamage(resolvedBaseDamage);
if (currentBaseHp > 0)
{
return;
}
EnterFinishFlow("Combat ended because base HP reached zero.", false);
}
private void ResetRuntime()
{
_phaseBuffer.Clear();
_spawnEntriesByPhaseId.Clear();
_phaseLoopRuntime.Reset();
_loadSession.Reset();
_combatResourceManager.Reset();
_currentLevel = null;
_isFinishAsVictory = true;
_state = SchedulerState.Idle;
}
private void CleanupAllCombatEntities()
{
_loadSession.Cleanup();
_enemyManager.CleanupTrackedEnemies();
}
private void EnsureCombatFinishFormUseCaseBound()
{
_combatFinishFormUseCase ??= new CombatFinishFormUseCase(this);
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase);
}
private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory)
{
EnsureCombatFinishFormUseCaseBound();
_combatFinishFormUseCase.SetSummary(
defeatedEnemyCount,
gainedGold,
rewardInventory);
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
}
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
{
_combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold);
if (_state != SchedulerState.RunningPhase)
{
return;
}
_combatResourceManager.TryRollOutGameItemDrop(
_phaseLoopRuntime.DisplayPhaseIndex,
ResolveCurrentThemeType());
}
private LevelThemeType ResolveCurrentThemeType()
{
if (_currentLevel != null)
{
return _currentLevel.LevelThemeType;
}
if (GameEntry.CombatNode != null)
{
return GameEntry.CombatNode.CurrentThemeType;
}
return LevelThemeType.None;
}
private void CloseCombatFinishForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
}
public bool OnCombatFinishReturnRequested()
{
if (_state != SchedulerState.WaitingForFinishReturn)
{
return false;
}
// Step 2: clear remaining map/UI resources and close finish form.
_loadSession.Cleanup();
CloseCombatFinishForm();
_state = SchedulerState.Completed;
GameEntry.CombatNode?.OnCombatEndedByScheduler(_isFinishAsVictory);
return true;
}
private bool HandleStartFailure(string errorMessage)
{
Log.Warning("{0}", errorMessage);
_state = SchedulerState.Failed;
_enemyManager.EndPhase();
CleanupAllCombatEntities();
CloseCombatFinishForm();
ResetRuntime();
return false;
}
private void EnterFailureFallback(string errorMessage)
{
if (_state == SchedulerState.Failed || _state == SchedulerState.Completed)
{
return;
}
_state = SchedulerState.Failed;
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0,
errorMessage);
_enemyManager.EndPhase();
CleanupAllCombatEntities();
CloseCombatFinishForm();
GameEntry.CombatNode?.OnCombatEndedByScheduler(false);
}
private static int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount)
{
if (displayPhaseIndex <= 0 || phaseCount <= 0)
{
return 1;
}
int completedLoopCount = Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount);
if (completedLoopCount >= 30)
{
return int.MaxValue;
}
return 1 << completedLoopCount;
}
#region Event Handlers
private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args)
{
var status = _loadSession.HandleShowEntitySuccess(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
EnterFailureFallback(errorMessage);
return;
}
if (status == CombatLoadSession.EventHandleStatus.Succeeded)
{
MapEntity map = _loadSession.CurrentMap;
Log.Info(
"Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.",
_currentLevel != null ? _currentLevel.Id : 0,
map.PathCells.Count,
map.FoundationCells.Count,
map.Spawners.Length,
map.House != null ? map.House.name : "None");
}
}
private void OnShowEntityFailure(ShowEntityFailureEventArgs args)
{
var status = _loadSession.HandleShowEntityFailure(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
EnterFailureFallback(errorMessage);
}
}
private void OnHideEntityComplete(HideEntityCompleteEventArgs args)
{
_loadSession.HandleHideEntityComplete(args);
}
private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args)
{
var status = _loadSession.HandleOpenUIFormSuccess(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
EnterFailureFallback(errorMessage);
}
}
private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args)
{
var status = _loadSession.HandleOpenUIFormFailure(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
EnterFailureFallback(errorMessage);
}
}
private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
{
_loadSession.HandleCloseUIFormComplete(args);
}
#endregion
}
}