拆分 CombatScheduler 状态机到独立文件

This commit is contained in:
SepComet 2026-03-07 11:21:07 +08:00
parent 34446ae42a
commit 0ff04f02f4
23 changed files with 664 additions and 231 deletions

View File

@ -240,7 +240,6 @@ namespace GeometryTD.CustomComponent
} }
_isCombatActive = true; _isCombatActive = true;
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
} }
public void EndCombat() public void EndCombat()
@ -506,4 +505,3 @@ namespace GeometryTD.CustomComponent
} }
} }

View File

@ -10,7 +10,7 @@ using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
public class CombatScheduler public partial class CombatScheduler
{ {
private const int RewardSelectDisplayCount = 3; private const int RewardSelectDisplayCount = 3;
private const float FullBaseHpGoldBonusRate = 0.3f; private const float FullBaseHpGoldBonusRate = 0.3f;
@ -19,16 +19,6 @@ namespace GeometryTD.CustomComponent
private const float MidBaseHpThreshold = 0.5f; private const float MidBaseHpThreshold = 0.5f;
private const float LowBaseHpTowerEndurancePenalty = 10f; private const float LowBaseHpTowerEndurancePenalty = 10f;
private enum SchedulerState : byte
{
Idle = 0,
WaitingForLoading = 1,
RunningPhase = 2,
WaitingForFinishReturn = 3,
Completed = 4,
Failed = 5
}
private readonly List<DRLevelPhase> _phaseBuffer = new(); private readonly List<DRLevelPhase> _phaseBuffer = new();
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new(); private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
private readonly EnemyManager _enemyManager = new(); private readonly EnemyManager _enemyManager = new();
@ -41,16 +31,17 @@ namespace GeometryTD.CustomComponent
private DRLevel _currentLevel; private DRLevel _currentLevel;
private CombatFinishFormUseCase _combatFinishFormUseCase; private CombatFinishFormUseCase _combatFinishFormUseCase;
private RewardSelectFormUseCase _rewardSelectFormUseCase; private RewardSelectFormUseCase _rewardSelectFormUseCase;
private SchedulerState _state = SchedulerState.Idle; private CombatStateBase _currentState;
private bool _initialized; private bool _initialized;
private bool _isFinishAsVictory = true; private bool _isFinishAsVictory = true;
private int _pendingFinishDefeatedEnemyCount; private bool _isCompleted;
private int _pendingFinishGainedGold; private bool _nodeEnterFired;
private BackpackInventoryData _pendingFinishRewardInventory; private SettlementContext _settlementContext;
private bool _hasPendingFinishSettlement;
public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase; public bool IsRunning =>
public bool IsCompleted => _state == SchedulerState.Completed; _currentState is CombatLoadingState or CombatRunningPhaseState or CombatWaitingForPhaseEndState;
public bool IsCompleted => _isCompleted;
public DRLevel CurrentLevel => _currentLevel; public DRLevel CurrentLevel => _currentLevel;
public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase; public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase;
public MapEntity CurrentMap => _loadSession.CurrentMap; public MapEntity CurrentMap => _loadSession.CurrentMap;
@ -136,30 +127,17 @@ namespace GeometryTD.CustomComponent
return HandleStartFailure($"CombatScheduler start failed. {loadError}"); return HandleStartFailure($"CombatScheduler start failed. {loadError}");
} }
_state = SchedulerState.WaitingForLoading; ChangeState(new CombatLoadingState(this));
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, Log.Info(
"CombatScheduler started. Level={0}, PhaseCount={1}.",
_currentLevel.Id,
_phaseLoopRuntime.PhaseCount); _phaseLoopRuntime.PhaseCount);
return true; return true;
} }
public void OnUpdate(float elapseSeconds, float realElapseSeconds) public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{ {
switch (_state) _currentState?.OnUpdate(elapseSeconds, realElapseSeconds);
{
case SchedulerState.WaitingForLoading:
if (!_loadSession.IsReady)
{
return;
}
BeginNextPhase();
return;
case SchedulerState.RunningPhase:
UpdateCurrentPhase(elapseSeconds, realElapseSeconds);
return;
default:
return;
}
} }
public void OnDestroy() public void OnDestroy()
@ -169,6 +147,9 @@ namespace GeometryTD.CustomComponent
return; return;
} }
_currentState?.OnExit();
_currentState?.OnDestroy();
_currentState = null;
CleanupAllCombatEntities(); CleanupAllCombatEntities();
CloseCombatFinishForm(); CloseCombatFinishForm();
CloseRewardSelectForm(); CloseRewardSelectForm();
@ -184,110 +165,18 @@ namespace GeometryTD.CustomComponent
public bool TryEndCombatByPlayer() public bool TryEndCombatByPlayer()
{ {
if (_state != SchedulerState.RunningPhase) if (_currentState is not CombatRunningPhaseState &&
_currentState is not CombatWaitingForPhaseEndState)
{ {
return false; return false;
} }
if (!_phaseLoopRuntime.TryRequestEndCombat()) return _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;
// Step 1: stop runtime and clear enemy entities only.
_enemyManager.EndPhase();
_state = SchedulerState.WaitingForFinishReturn;
_isFinishAsVictory = isVictory;
_enemyManager.CleanupTrackedEnemies();
bool shouldOpenFullBaseHpRewardSelect = false;
ApplySettlementModifierByBaseHp(isVictory, out shouldOpenFullBaseHpRewardSelect);
PreparePendingFinishSummary(defeatedEnemyCount);
Log.Info(
"CombatScheduler entered finish flow. Level={0}. Reason={1}",
_currentLevel != null ? _currentLevel.Id : 0,
reason);
if (shouldOpenFullBaseHpRewardSelect && TryOpenFullBaseHpRewardSelect())
{
return;
}
CommitPendingSettlementAndOpenFinishForm();
} }
public void OnEnemyReachedBase(int baseDamage) public void OnEnemyReachedBase(int baseDamage)
{ {
if (_state != SchedulerState.RunningPhase) if (!IsRunning)
{ {
return; return;
} }
@ -298,32 +187,47 @@ namespace GeometryTD.CustomComponent
return; return;
} }
CombatNodeComponent combatNode = GameEntry.CombatNode; ApplyBaseDamage(resolvedBaseDamage);
if (combatNode == null) }
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
{
_combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold);
if (!IsRunning)
{ {
return; return;
} }
int currentBaseHp = combatNode.ApplyBaseDamage(resolvedBaseDamage); _combatResourceManager.TryRollOutGameItemDrop(
if (currentBaseHp > 0) _phaseLoopRuntime.DisplayPhaseIndex,
{ ResolveCurrentThemeType());
return;
} }
EnterFinishFlow("Combat ended because base HP reached zero.", false); public bool OnCombatFinishReturnRequested()
{
if (_currentState is not CombatWaitingForReturnState waitingForReturnState)
{
return false;
}
waitingForReturnState.RequestReturn();
return true;
} }
private void ResetRuntime() private void ResetRuntime()
{ {
_currentState = null;
_phaseBuffer.Clear(); _phaseBuffer.Clear();
_spawnEntriesByPhaseId.Clear(); _spawnEntriesByPhaseId.Clear();
_phaseLoopRuntime.Reset(); _phaseLoopRuntime.Reset();
_loadSession.Reset(); _loadSession.Reset();
_combatResourceManager.Reset(); _combatResourceManager.Reset();
ClearPendingFinishContext(); _settlementContext = null;
_currentLevel = null; _currentLevel = null;
_isFinishAsVictory = true; _isFinishAsVictory = true;
_state = SchedulerState.Idle; _isCompleted = false;
_nodeEnterFired = false;
} }
private void CleanupAllCombatEntities() private void CleanupAllCombatEntities()
@ -335,7 +239,6 @@ namespace GeometryTD.CustomComponent
private void EnsureCombatFinishFormUseCaseBound() private void EnsureCombatFinishFormUseCaseBound()
{ {
_combatFinishFormUseCase ??= new CombatFinishFormUseCase(this); _combatFinishFormUseCase ??= new CombatFinishFormUseCase(this);
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase); GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase);
} }
@ -345,46 +248,120 @@ namespace GeometryTD.CustomComponent
GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _rewardSelectFormUseCase); GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _rewardSelectFormUseCase);
} }
private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory) private void ChangeState(CombatStateBase nextState)
{ {
EnsureCombatFinishFormUseCaseBound(); if (ReferenceEquals(_currentState, nextState))
_combatFinishFormUseCase.SetSummary(
defeatedEnemyCount,
gainedGold,
rewardInventory);
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
}
private void PreparePendingFinishSummary(int defeatedEnemyCount)
{
_pendingFinishDefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount);
_pendingFinishGainedGold = Mathf.Max(0, _combatResourceManager.GainedGold);
_pendingFinishRewardInventory = _combatResourceManager.GetRewardInventorySnapshot();
_hasPendingFinishSettlement = true;
}
private void CommitPendingSettlementAndOpenFinishForm()
{
if (!_hasPendingFinishSettlement)
{ {
return; return;
} }
BackpackInventoryData rewardInventory = _pendingFinishRewardInventory ?? new BackpackInventoryData(); _currentState?.OnExit();
int defeatedEnemyCount = _pendingFinishDefeatedEnemyCount; _currentState?.OnDestroy();
int gainedGold = _pendingFinishGainedGold; _currentState = nextState;
_currentState?.OnInit();
GameEntry.PlayerInventory?.MergeInventory(rewardInventory); _currentState?.OnEnter();
OpenCombatFinishForm(defeatedEnemyCount, gainedGold, rewardInventory);
ClearPendingFinishContext();
} }
private void ClearPendingFinishContext() private bool TryBeginNextPhase()
{ {
_pendingFinishDefeatedEnemyCount = 0; if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
_pendingFinishGainedGold = 0; {
_pendingFinishRewardInventory = null; ChangeState(new CombatSettlementState(this, "Combat ended after loop completion.", true));
_hasPendingFinishSettlement = false; return false;
}
_spawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries);
ChangeState(new CombatRunningPhaseState(this, nextPhase, spawnEntries));
return true;
}
private void EnterWaitingForPhaseEnd()
{
ChangeState(new CombatWaitingForPhaseEndState(this));
}
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);
TryBeginNextPhase();
}
private bool ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory)
{
if (GetCurrentBaseHp() <= 0)
{
reason = "Combat ended because base HP reached zero.";
isVictory = false;
return true;
}
if (_phaseLoopRuntime.IsEndCombatRequested)
{
reason = "Combat ended by player.";
isVictory = true;
return true;
}
reason = null;
isVictory = true;
return false;
}
private void OpenCombatFinishForm(SettlementContext settlementContext)
{
EnsureCombatFinishFormUseCaseBound();
_combatFinishFormUseCase.SetSummary(
settlementContext.DefeatedEnemyCount,
settlementContext.GainedGold,
settlementContext.RewardInventory);
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
}
private SettlementContext BuildSettlementContext(string reason, bool isVictory)
{
int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount;
_enemyManager.EndPhase();
_enemyManager.CleanupTrackedEnemies();
_isFinishAsVictory = isVictory;
bool shouldOpenFullBaseHpRewardSelect = false;
ApplySettlementModifierByBaseHp(isVictory, out shouldOpenFullBaseHpRewardSelect);
SettlementContext settlementContext = new SettlementContext
{
DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount),
GainedGold = Mathf.Max(0, _combatResourceManager.GainedGold),
RewardInventory = _combatResourceManager.GetRewardInventorySnapshot(),
ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect,
Reason = reason
};
Log.Info(
"CombatScheduler entered finish flow. Level={0}. Reason={1}",
_currentLevel != null ? _currentLevel.Id : 0,
reason);
return settlementContext;
}
private void CommitSettlementInventory(SettlementContext settlementContext)
{
if (settlementContext == null || settlementContext.IsCommitted)
{
return;
}
BackpackInventoryData rewardInventory = settlementContext.RewardInventory ?? new BackpackInventoryData();
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
settlementContext.RewardInventory = rewardInventory;
settlementContext.IsCommitted = true;
} }
private void ApplySettlementModifierByBaseHp(bool isVictory, out bool shouldOpenFullBaseHpRewardSelect) private void ApplySettlementModifierByBaseHp(bool isVictory, out bool shouldOpenFullBaseHpRewardSelect)
@ -396,8 +373,8 @@ namespace GeometryTD.CustomComponent
} }
int levelRewardGold = _currentLevel != null ? Mathf.Max(0, _currentLevel.RewardGold) : 0; int levelRewardGold = _currentLevel != null ? Mathf.Max(0, _currentLevel.RewardGold) : 0;
int currentBaseHp = 0; int currentBaseHp;
int maxBaseHp = 0; int maxBaseHp;
float bonusRate = 0f; float bonusRate = 0f;
bool appliedLowBaseHpPenalty = false; bool appliedLowBaseHpPenalty = false;
@ -438,19 +415,8 @@ namespace GeometryTD.CustomComponent
private void ResolveBaseHpSnapshot(out int currentBaseHp, out int maxBaseHp) private void ResolveBaseHpSnapshot(out int currentBaseHp, out int maxBaseHp)
{ {
currentBaseHp = 0; currentBaseHp = Mathf.Max(0, GetCurrentBaseHp());
maxBaseHp = _currentLevel != null ? Mathf.Max(0, _currentLevel.BaseHp) : 0; maxBaseHp = _currentLevel != null ? Mathf.Max(0, _currentLevel.BaseHp) : 0;
CombatNodeComponent combatNode = GameEntry.CombatNode;
if (combatNode != null)
{
currentBaseHp = Mathf.Max(0, combatNode.CurrentBaseHp);
if (maxBaseHp <= 0 && combatNode.CurrentLevel != null)
{
maxBaseHp = Mathf.Max(0, combatNode.CurrentLevel.BaseHp);
}
}
if (maxBaseHp > 0) if (maxBaseHp > 0)
{ {
currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp); currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp);
@ -469,7 +435,7 @@ namespace GeometryTD.CustomComponent
return affectedTowerCount > 0; return affectedTowerCount > 0;
} }
private bool TryOpenFullBaseHpRewardSelect() private bool TryPrepareRewardSelection(SettlementContext settlementContext)
{ {
IReadOnlyList<TowerCompItemData> candidateItems = _combatResourceManager.RollSettlementRewardCandidates( IReadOnlyList<TowerCompItemData> candidateItems = _combatResourceManager.RollSettlementRewardCandidates(
_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.DisplayPhaseIndex,
@ -477,6 +443,7 @@ namespace GeometryTD.CustomComponent
RewardSelectDisplayCount); RewardSelectDisplayCount);
if (candidateItems == null || candidateItems.Count <= 0) if (candidateItems == null || candidateItems.Count <= 0)
{ {
settlementContext.ShouldOpenRewardSelection = false;
return false; return false;
} }
@ -494,6 +461,7 @@ namespace GeometryTD.CustomComponent
if (rewardPool.Count <= 0) if (rewardPool.Count <= 0)
{ {
settlementContext.ShouldOpenRewardSelection = false;
return false; return false;
} }
@ -510,6 +478,7 @@ namespace GeometryTD.CustomComponent
RewardSelectFormRawData rawData = _rewardSelectFormUseCase.CreateInitialModel(); RewardSelectFormRawData rawData = _rewardSelectFormUseCase.CreateInitialModel();
if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0) if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0)
{ {
settlementContext.ShouldOpenRewardSelection = false;
return false; return false;
} }
@ -519,17 +488,27 @@ namespace GeometryTD.CustomComponent
private void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward) private void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward)
{ {
if (_pendingFinishRewardInventory != null && selectedReward?.SourceItem != null) if (_currentState is not CombatRewardSelectionState || _settlementContext == null)
{ {
TryAppendRewardComponent(_pendingFinishRewardInventory, selectedReward.SourceItem); return;
} }
CommitPendingSettlementAndOpenFinishForm(); if (_settlementContext.RewardInventory != null && selectedReward?.SourceItem != null)
{
TryAppendRewardComponent(_settlementContext.RewardInventory, selectedReward.SourceItem);
}
ChangeState(new CombatFinishFormState(this));
} }
private void OnFullBaseHpRewardGiveUp() private void OnFullBaseHpRewardGiveUp()
{ {
CommitPendingSettlementAndOpenFinishForm(); if (_currentState is not CombatRewardSelectionState || _settlementContext == null)
{
return;
}
ChangeState(new CombatFinishFormState(this));
} }
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem) private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)
@ -613,20 +592,6 @@ namespace GeometryTD.CustomComponent
return string.Empty; return string.Empty;
} }
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
{
_combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold);
if (_state != SchedulerState.RunningPhase)
{
return;
}
_combatResourceManager.TryRollOutGameItemDrop(
_phaseLoopRuntime.DisplayPhaseIndex,
ResolveCurrentThemeType());
}
private LevelThemeType ResolveCurrentThemeType() private LevelThemeType ResolveCurrentThemeType()
{ {
if (_currentLevel != null) if (_currentLevel != null)
@ -642,6 +607,28 @@ namespace GeometryTD.CustomComponent
return LevelThemeType.None; return LevelThemeType.None;
} }
private int ApplyBaseDamage(int damage)
{
CombatNodeComponent combatNode = GameEntry.CombatNode;
if (combatNode == null)
{
return 0;
}
return combatNode.ApplyBaseDamage(damage);
}
private int GetCurrentBaseHp()
{
CombatNodeComponent combatNode = GameEntry.CombatNode;
if (combatNode == null)
{
return 0;
}
return Mathf.Max(0, combatNode.CurrentBaseHp);
}
private void CloseCombatFinishForm() private void CloseCombatFinishForm()
{ {
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm); GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
@ -652,26 +639,16 @@ namespace GeometryTD.CustomComponent
GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm); GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm);
} }
public bool OnCombatFinishReturnRequested() private void CompleteCombatAndNotify(bool succeeded)
{ {
if (_state != SchedulerState.WaitingForFinishReturn) _isCompleted = true;
{ _currentState = null;
return false; GameEntry.CombatNode?.OnCombatEndedByScheduler(succeeded);
}
// Step 2: clear remaining map/UI resources and close finish form.
_loadSession.Cleanup();
CloseCombatFinishForm();
CloseRewardSelectForm();
_state = SchedulerState.Completed;
GameEntry.CombatNode?.OnCombatEndedByScheduler(_isFinishAsVictory);
return true;
} }
private bool HandleStartFailure(string errorMessage) private bool HandleStartFailure(string errorMessage)
{ {
Log.Warning("{0}", errorMessage); Log.Warning("{0}", errorMessage);
_state = SchedulerState.Failed;
_enemyManager.EndPhase(); _enemyManager.EndPhase();
CleanupAllCombatEntities(); CleanupAllCombatEntities();
CloseCombatFinishForm(); CloseCombatFinishForm();
@ -682,19 +659,12 @@ namespace GeometryTD.CustomComponent
private void EnterFailureFallback(string errorMessage) private void EnterFailureFallback(string errorMessage)
{ {
if (_state == SchedulerState.Failed || _state == SchedulerState.Completed) if (_currentState is CombatFailedState || _isCompleted)
{ {
return; return;
} }
_state = SchedulerState.Failed; ChangeState(new CombatFailedState(this, errorMessage, true));
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0,
errorMessage);
_enemyManager.EndPhase();
CleanupAllCombatEntities();
CloseCombatFinishForm();
CloseRewardSelectForm();
GameEntry.CombatNode?.OnCombatEndedByScheduler(false);
} }
private static int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount) private static int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount)
@ -775,5 +745,21 @@ namespace GeometryTD.CustomComponent
} }
#endregion #endregion
private void GameOverByFailure()
{
CompleteCombatAndNotify(false);
}
private sealed class SettlementContext
{
public int DefeatedEnemyCount;
public int GainedGold;
public BackpackInventoryData RewardInventory;
public bool ShouldOpenRewardSelection;
public bool IsCommitted;
public string Reason;
}
} }
} }

View File

@ -21,6 +21,7 @@ namespace GeometryTD.CustomComponent
public DRLevelPhase CurrentPhase => _currentPhase; public DRLevelPhase CurrentPhase => _currentPhase;
public int DisplayPhaseIndex => _displayPhaseIndex; public int DisplayPhaseIndex => _displayPhaseIndex;
public bool CanEndCombat => _canEndCombat; public bool CanEndCombat => _canEndCombat;
public bool IsEndCombatRequested => _endCombatRequested;
public float CurrentPhaseElapsed => _currentPhaseElapsed; public float CurrentPhaseElapsed => _currentPhaseElapsed;
public int PhaseCount => _phases.Count; public int PhaseCount => _phases.Count;

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 76973ced080a4a839543fc9361b6b96b
timeCreated: 1772847309

View File

@ -0,0 +1,35 @@
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private sealed class CombatFailedState : CombatStateBase
{
private readonly string _errorMessage;
private readonly bool _notifyCombatNode;
public CombatFailedState(CombatScheduler scheduler, string errorMessage, bool notifyCombatNode) : base(scheduler)
{
_errorMessage = errorMessage;
_notifyCombatNode = notifyCombatNode;
}
public override void OnEnter()
{
Log.Error(
"CombatScheduler failed. LevelId={0}, {1}",
Scheduler._currentLevel != null ? Scheduler._currentLevel.Id : 0,
_errorMessage);
Scheduler._enemyManager.EndPhase();
Scheduler.CleanupAllCombatEntities();
Scheduler.CloseCombatFinishForm();
Scheduler.CloseRewardSelectForm();
if (_notifyCombatNode)
{
Scheduler.GameOverByFailure();
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39d988ff69b018a49857384e3a3b6f20
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,28 @@
using UnityEngine;
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private sealed class CombatFinishFormState : CombatStateBase
{
public CombatFinishFormState(CombatScheduler scheduler) : base(scheduler)
{
}
public override void OnEnter()
{
if (Scheduler._settlementContext == null)
{
Scheduler.EnterFailureFallback("Combat finish form failed. Settlement context is missing.");
return;
}
Scheduler.CommitSettlementInventory(Scheduler._settlementContext);
Scheduler._settlementContext.GainedGold = Mathf.Max(0, Scheduler._combatResourceManager.GainedGold);
Scheduler.OpenCombatFinishForm(Scheduler._settlementContext);
Scheduler.ChangeState(new CombatWaitingForReturnState(Scheduler));
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d78014e1fa53434aa14628f83ed3669
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,25 @@
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private sealed class CombatLoadingState : CombatStateBase
{
public CombatLoadingState(CombatScheduler scheduler) : base(scheduler)
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_ = elapseSeconds;
_ = realElapseSeconds;
if (!Scheduler._loadSession.IsReady)
{
return;
}
Scheduler.TryBeginNextPhase();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f604bf4c97e462abf153804c1b0b55d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,31 @@
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private sealed class CombatRewardSelectionState : CombatStateBase
{
public CombatRewardSelectionState(CombatScheduler scheduler) : base(scheduler)
{
}
public override void OnEnter()
{
if (Scheduler._settlementContext == null)
{
Scheduler.EnterFailureFallback("Combat reward selection failed. Settlement context is missing.");
return;
}
if (!Scheduler.TryPrepareRewardSelection(Scheduler._settlementContext))
{
Scheduler.ChangeState(new CombatFinishFormState(Scheduler));
}
}
public override void OnExit()
{
Scheduler.CloseRewardSelectForm();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e087635834ae66943a6fc76e813ab1f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,77 @@
using System.Collections.Generic;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private sealed class CombatRunningPhaseState : CombatStateBase
{
private readonly DRLevelPhase _phase;
private readonly IReadOnlyList<DRLevelSpawnEntry> _spawnEntries;
public CombatRunningPhaseState(
CombatScheduler scheduler,
DRLevelPhase phase,
IReadOnlyList<DRLevelSpawnEntry> spawnEntries) : base(scheduler)
{
_phase = phase;
_spawnEntries = spawnEntries;
}
public override void OnEnter()
{
Scheduler._enemyManager.BeginPhase(_phase, _spawnEntries);
GameEntry.Event.Fire(
Scheduler,
CombatProcessEventArgs.Create(
Scheduler._phaseLoopRuntime.DisplayPhaseIndex,
Scheduler._phaseLoopRuntime.PhaseCount));
GameEntry.Event.Fire(
Scheduler,
CombatEnemyHpRateChangedEventArgs.Create(
ResolveEnemyHpRateMultiplier(
Scheduler._phaseLoopRuntime.DisplayPhaseIndex,
Scheduler._phaseLoopRuntime.PhaseCount)));
if (!Scheduler._nodeEnterFired)
{
Scheduler._nodeEnterFired = true;
GameEntry.Event.Fire(Scheduler, NodeEnterEventArgs.Create());
}
Log.Info(
"CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.",
Scheduler._currentLevel != null ? Scheduler._currentLevel.Id : 0,
Scheduler._phaseLoopRuntime.DisplayPhaseIndex,
_phase.EndType,
_spawnEntries != null ? _spawnEntries.Count : 0);
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (Scheduler._phaseLoopRuntime.CurrentPhase == null)
{
Scheduler.EnterFailureFallback("CombatScheduler update failed. Current phase is null.");
return;
}
Scheduler._phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
Scheduler._enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
if (Scheduler.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory))
{
Scheduler.ChangeState(new CombatSettlementState(Scheduler, reason, isVictory));
return;
}
if (Scheduler._enemyManager.IsPhaseSpawnCompleted)
{
Scheduler.EnterWaitingForPhaseEnd();
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ea3f65c4dc4478ab57c6b8d7a5382b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,29 @@
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private sealed class CombatSettlementState : CombatStateBase
{
private readonly string _reason;
private readonly bool _isVictory;
public CombatSettlementState(CombatScheduler scheduler, string reason, bool isVictory) : base(scheduler)
{
_reason = reason;
_isVictory = isVictory;
}
public override void OnEnter()
{
Scheduler._settlementContext = Scheduler.BuildSettlementContext(_reason, _isVictory);
if (Scheduler._settlementContext.ShouldOpenRewardSelection)
{
Scheduler.ChangeState(new CombatRewardSelectionState(Scheduler));
return;
}
Scheduler.ChangeState(new CombatFinishFormState(Scheduler));
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 61773c700734bfb4db073594ef2f97a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,35 @@
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private abstract class CombatStateBase
{
protected CombatScheduler Scheduler { get; }
protected CombatStateBase(CombatScheduler scheduler)
{
Scheduler = scheduler;
}
public virtual void OnInit()
{
}
public virtual void OnEnter()
{
}
public virtual void OnExit()
{
}
public virtual void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
}
public virtual void OnDestroy()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9346a56a6d174c3fbc6889003134ddc6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,40 @@
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private sealed class CombatWaitingForPhaseEndState : CombatStateBase
{
public CombatWaitingForPhaseEndState(CombatScheduler scheduler) : base(scheduler)
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_ = realElapseSeconds;
if (Scheduler._phaseLoopRuntime.CurrentPhase == null)
{
Scheduler.EnterFailureFallback("CombatScheduler waiting phase failed. Current phase is null.");
return;
}
Scheduler._phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
if (Scheduler.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory))
{
Scheduler.ChangeState(new CombatSettlementState(Scheduler, reason, isVictory));
return;
}
if (!Scheduler._phaseLoopRuntime.ShouldEndCurrentPhase(
Scheduler._enemyManager.IsPhaseSpawnCompleted,
Scheduler._enemyManager.AliveEnemyCount))
{
return;
}
Scheduler.CompleteCurrentPhase();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 190af076775ad1b4c97a2baf09588bbf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,35 @@
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
{
private sealed class CombatWaitingForReturnState : CombatStateBase
{
private bool _returnRequested;
public CombatWaitingForReturnState(CombatScheduler scheduler) : base(scheduler)
{
}
public void RequestReturn()
{
_returnRequested = true;
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_ = elapseSeconds;
_ = realElapseSeconds;
if (!_returnRequested)
{
return;
}
Scheduler._loadSession.Cleanup();
Scheduler.CloseCombatFinishForm();
Scheduler.CloseRewardSelectForm();
Scheduler.CompleteCombatAndNotify(Scheduler._isFinishAsVictory);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c86116fef176a840a38bd7c154a5deb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b957db4a74448154fbe3fbec36c33038
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: