拆分 CombatScheduler 状态机到独立文件
This commit is contained in:
parent
34446ae42a
commit
0ff04f02f4
|
|
@ -240,7 +240,6 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
|
||||
_isCombatActive = true;
|
||||
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
|
||||
}
|
||||
|
||||
public void EndCombat()
|
||||
|
|
@ -506,4 +505,3 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ using UnityGameFramework.Runtime;
|
|||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class CombatScheduler
|
||||
public partial class CombatScheduler
|
||||
{
|
||||
private const int RewardSelectDisplayCount = 3;
|
||||
private const float FullBaseHpGoldBonusRate = 0.3f;
|
||||
|
|
@ -19,16 +19,6 @@ namespace GeometryTD.CustomComponent
|
|||
private const float MidBaseHpThreshold = 0.5f;
|
||||
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 Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
|
||||
private readonly EnemyManager _enemyManager = new();
|
||||
|
|
@ -41,16 +31,17 @@ namespace GeometryTD.CustomComponent
|
|||
private DRLevel _currentLevel;
|
||||
private CombatFinishFormUseCase _combatFinishFormUseCase;
|
||||
private RewardSelectFormUseCase _rewardSelectFormUseCase;
|
||||
private SchedulerState _state = SchedulerState.Idle;
|
||||
private CombatStateBase _currentState;
|
||||
private bool _initialized;
|
||||
private bool _isFinishAsVictory = true;
|
||||
private int _pendingFinishDefeatedEnemyCount;
|
||||
private int _pendingFinishGainedGold;
|
||||
private BackpackInventoryData _pendingFinishRewardInventory;
|
||||
private bool _hasPendingFinishSettlement;
|
||||
private bool _isCompleted;
|
||||
private bool _nodeEnterFired;
|
||||
private SettlementContext _settlementContext;
|
||||
|
||||
public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase;
|
||||
public bool IsCompleted => _state == SchedulerState.Completed;
|
||||
public bool IsRunning =>
|
||||
_currentState is CombatLoadingState or CombatRunningPhaseState or CombatWaitingForPhaseEndState;
|
||||
|
||||
public bool IsCompleted => _isCompleted;
|
||||
public DRLevel CurrentLevel => _currentLevel;
|
||||
public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase;
|
||||
public MapEntity CurrentMap => _loadSession.CurrentMap;
|
||||
|
|
@ -136,30 +127,17 @@ namespace GeometryTD.CustomComponent
|
|||
return HandleStartFailure($"CombatScheduler start failed. {loadError}");
|
||||
}
|
||||
|
||||
_state = SchedulerState.WaitingForLoading;
|
||||
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id,
|
||||
ChangeState(new CombatLoadingState(this));
|
||||
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;
|
||||
}
|
||||
_currentState?.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
|
|
@ -169,6 +147,9 @@ namespace GeometryTD.CustomComponent
|
|||
return;
|
||||
}
|
||||
|
||||
_currentState?.OnExit();
|
||||
_currentState?.OnDestroy();
|
||||
_currentState = null;
|
||||
CleanupAllCombatEntities();
|
||||
CloseCombatFinishForm();
|
||||
CloseRewardSelectForm();
|
||||
|
|
@ -184,110 +165,18 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
public bool TryEndCombatByPlayer()
|
||||
{
|
||||
if (_state != SchedulerState.RunningPhase)
|
||||
if (_currentState is not CombatRunningPhaseState &&
|
||||
_currentState is not CombatWaitingForPhaseEndState)
|
||||
{
|
||||
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;
|
||||
|
||||
// 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();
|
||||
return _phaseLoopRuntime.TryRequestEndCombat();
|
||||
}
|
||||
|
||||
public void OnEnemyReachedBase(int baseDamage)
|
||||
{
|
||||
if (_state != SchedulerState.RunningPhase)
|
||||
if (!IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -298,32 +187,47 @@ namespace GeometryTD.CustomComponent
|
|||
return;
|
||||
}
|
||||
|
||||
CombatNodeComponent combatNode = GameEntry.CombatNode;
|
||||
if (combatNode == null)
|
||||
ApplyBaseDamage(resolvedBaseDamage);
|
||||
}
|
||||
|
||||
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
|
||||
{
|
||||
_combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold);
|
||||
|
||||
if (!IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int currentBaseHp = combatNode.ApplyBaseDamage(resolvedBaseDamage);
|
||||
if (currentBaseHp > 0)
|
||||
{
|
||||
return;
|
||||
_combatResourceManager.TryRollOutGameItemDrop(
|
||||
_phaseLoopRuntime.DisplayPhaseIndex,
|
||||
ResolveCurrentThemeType());
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
_currentState = null;
|
||||
_phaseBuffer.Clear();
|
||||
_spawnEntriesByPhaseId.Clear();
|
||||
_phaseLoopRuntime.Reset();
|
||||
_loadSession.Reset();
|
||||
_combatResourceManager.Reset();
|
||||
ClearPendingFinishContext();
|
||||
_settlementContext = null;
|
||||
_currentLevel = null;
|
||||
_isFinishAsVictory = true;
|
||||
_state = SchedulerState.Idle;
|
||||
_isCompleted = false;
|
||||
_nodeEnterFired = false;
|
||||
}
|
||||
|
||||
private void CleanupAllCombatEntities()
|
||||
|
|
@ -335,7 +239,6 @@ namespace GeometryTD.CustomComponent
|
|||
private void EnsureCombatFinishFormUseCaseBound()
|
||||
{
|
||||
_combatFinishFormUseCase ??= new CombatFinishFormUseCase(this);
|
||||
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase);
|
||||
}
|
||||
|
||||
|
|
@ -345,46 +248,120 @@ namespace GeometryTD.CustomComponent
|
|||
GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _rewardSelectFormUseCase);
|
||||
}
|
||||
|
||||
private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory)
|
||||
private void ChangeState(CombatStateBase nextState)
|
||||
{
|
||||
EnsureCombatFinishFormUseCaseBound();
|
||||
_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)
|
||||
if (ReferenceEquals(_currentState, nextState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BackpackInventoryData rewardInventory = _pendingFinishRewardInventory ?? new BackpackInventoryData();
|
||||
int defeatedEnemyCount = _pendingFinishDefeatedEnemyCount;
|
||||
int gainedGold = _pendingFinishGainedGold;
|
||||
|
||||
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
|
||||
OpenCombatFinishForm(defeatedEnemyCount, gainedGold, rewardInventory);
|
||||
ClearPendingFinishContext();
|
||||
_currentState?.OnExit();
|
||||
_currentState?.OnDestroy();
|
||||
_currentState = nextState;
|
||||
_currentState?.OnInit();
|
||||
_currentState?.OnEnter();
|
||||
}
|
||||
|
||||
private void ClearPendingFinishContext()
|
||||
private bool TryBeginNextPhase()
|
||||
{
|
||||
_pendingFinishDefeatedEnemyCount = 0;
|
||||
_pendingFinishGainedGold = 0;
|
||||
_pendingFinishRewardInventory = null;
|
||||
_hasPendingFinishSettlement = false;
|
||||
if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
|
||||
{
|
||||
ChangeState(new CombatSettlementState(this, "Combat ended after loop completion.", true));
|
||||
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)
|
||||
|
|
@ -396,8 +373,8 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
|
||||
int levelRewardGold = _currentLevel != null ? Mathf.Max(0, _currentLevel.RewardGold) : 0;
|
||||
int currentBaseHp = 0;
|
||||
int maxBaseHp = 0;
|
||||
int currentBaseHp;
|
||||
int maxBaseHp;
|
||||
float bonusRate = 0f;
|
||||
bool appliedLowBaseHpPenalty = false;
|
||||
|
||||
|
|
@ -438,19 +415,8 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp);
|
||||
|
|
@ -469,7 +435,7 @@ namespace GeometryTD.CustomComponent
|
|||
return affectedTowerCount > 0;
|
||||
}
|
||||
|
||||
private bool TryOpenFullBaseHpRewardSelect()
|
||||
private bool TryPrepareRewardSelection(SettlementContext settlementContext)
|
||||
{
|
||||
IReadOnlyList<TowerCompItemData> candidateItems = _combatResourceManager.RollSettlementRewardCandidates(
|
||||
_phaseLoopRuntime.DisplayPhaseIndex,
|
||||
|
|
@ -477,6 +443,7 @@ namespace GeometryTD.CustomComponent
|
|||
RewardSelectDisplayCount);
|
||||
if (candidateItems == null || candidateItems.Count <= 0)
|
||||
{
|
||||
settlementContext.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -494,6 +461,7 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
if (rewardPool.Count <= 0)
|
||||
{
|
||||
settlementContext.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -510,6 +478,7 @@ namespace GeometryTD.CustomComponent
|
|||
RewardSelectFormRawData rawData = _rewardSelectFormUseCase.CreateInitialModel();
|
||||
if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0)
|
||||
{
|
||||
settlementContext.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -519,17 +488,27 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
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()
|
||||
{
|
||||
CommitPendingSettlementAndOpenFinishForm();
|
||||
if (_currentState is not CombatRewardSelectionState || _settlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ChangeState(new CombatFinishFormState(this));
|
||||
}
|
||||
|
||||
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)
|
||||
|
|
@ -613,20 +592,6 @@ namespace GeometryTD.CustomComponent
|
|||
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()
|
||||
{
|
||||
if (_currentLevel != null)
|
||||
|
|
@ -642,6 +607,28 @@ namespace GeometryTD.CustomComponent
|
|||
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()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
|
||||
|
|
@ -652,26 +639,16 @@ namespace GeometryTD.CustomComponent
|
|||
GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm);
|
||||
}
|
||||
|
||||
public bool OnCombatFinishReturnRequested()
|
||||
private void CompleteCombatAndNotify(bool succeeded)
|
||||
{
|
||||
if (_state != SchedulerState.WaitingForFinishReturn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: clear remaining map/UI resources and close finish form.
|
||||
_loadSession.Cleanup();
|
||||
CloseCombatFinishForm();
|
||||
CloseRewardSelectForm();
|
||||
_state = SchedulerState.Completed;
|
||||
GameEntry.CombatNode?.OnCombatEndedByScheduler(_isFinishAsVictory);
|
||||
return true;
|
||||
_isCompleted = true;
|
||||
_currentState = null;
|
||||
GameEntry.CombatNode?.OnCombatEndedByScheduler(succeeded);
|
||||
}
|
||||
|
||||
private bool HandleStartFailure(string errorMessage)
|
||||
{
|
||||
Log.Warning("{0}", errorMessage);
|
||||
_state = SchedulerState.Failed;
|
||||
_enemyManager.EndPhase();
|
||||
CleanupAllCombatEntities();
|
||||
CloseCombatFinishForm();
|
||||
|
|
@ -682,19 +659,12 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
private void EnterFailureFallback(string errorMessage)
|
||||
{
|
||||
if (_state == SchedulerState.Failed || _state == SchedulerState.Completed)
|
||||
if (_currentState is CombatFailedState || _isCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_state = SchedulerState.Failed;
|
||||
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0,
|
||||
errorMessage);
|
||||
_enemyManager.EndPhase();
|
||||
CleanupAllCombatEntities();
|
||||
CloseCombatFinishForm();
|
||||
CloseRewardSelectForm();
|
||||
GameEntry.CombatNode?.OnCombatEndedByScheduler(false);
|
||||
ChangeState(new CombatFailedState(this, errorMessage, true));
|
||||
}
|
||||
|
||||
private static int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount)
|
||||
|
|
@ -775,5 +745,21 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace GeometryTD.CustomComponent
|
|||
public DRLevelPhase CurrentPhase => _currentPhase;
|
||||
public int DisplayPhaseIndex => _displayPhaseIndex;
|
||||
public bool CanEndCombat => _canEndCombat;
|
||||
public bool IsEndCombatRequested => _endCombatRequested;
|
||||
public float CurrentPhaseElapsed => _currentPhaseElapsed;
|
||||
public int PhaseCount => _phases.Count;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 76973ced080a4a839543fc9361b6b96b
|
||||
timeCreated: 1772847309
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 39d988ff69b018a49857384e3a3b6f20
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0d78014e1fa53434aa14628f83ed3669
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f604bf4c97e462abf153804c1b0b55d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e087635834ae66943a6fc76e813ab1f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4ea3f65c4dc4478ab57c6b8d7a5382b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 61773c700734bfb4db073594ef2f97a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9346a56a6d174c3fbc6889003134ddc6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 190af076775ad1b4c97a2baf09588bbf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6c86116fef176a840a38bd7c154a5deb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b957db4a74448154fbe3fbec36c33038
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue