510 lines
17 KiB
C#
510 lines
17 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 partial class CombatScheduler
|
|
{
|
|
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 CombatInRunResourceManager _combatInRunResourceManager = new();
|
|
private readonly EnemyDropResolver _enemyDropResolver = new();
|
|
private readonly CombatSettlementFlowService _settlementFlowService = new();
|
|
|
|
private EntityComponent _entity;
|
|
private DRLevel _currentLevel;
|
|
private CombatFinishFormUseCase _combatFinishFormUseCase;
|
|
private RewardSelectFormUseCase _rewardSelectFormUseCase;
|
|
private CombatStateBase _currentState;
|
|
private bool _initialized;
|
|
private bool _isFinishAsVictory = true;
|
|
private bool _isCompleted;
|
|
private bool _nodeEnterFired;
|
|
private CombatSettlementContext _settlementContext;
|
|
|
|
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;
|
|
public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex;
|
|
public int PhaseCount => _phaseLoopRuntime.PhaseCount;
|
|
public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat;
|
|
public int CurrentCoin => _combatInRunResourceManager.CurrentCoin;
|
|
public int CurrentGold => _combatInRunResourceManager.CurrentGold;
|
|
public int CurrentBaseHp => _combatInRunResourceManager.CurrentBaseHp;
|
|
public int CurrentBuildTowerCount => _combatInRunResourceManager.CurrentBuildTowerCount;
|
|
public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount;
|
|
public int GainedCoin => _combatInRunResourceManager.GainedCoin;
|
|
public int GainedGold => _combatInRunResourceManager.GainedGold;
|
|
|
|
public void OnInit()
|
|
{
|
|
if (!_initialized)
|
|
{
|
|
_entity = GameEntry.Entity;
|
|
_eventBridge.Bind(
|
|
OnShowEntitySuccess,
|
|
OnShowEntityFailure,
|
|
OnHideEntityComplete,
|
|
OnOpenUIFormSuccess,
|
|
OnOpenUIFormFailure,
|
|
OnCloseUIFormComplete);
|
|
_enemyManager.OnInit(this);
|
|
_loadSession.OnInit(_entity);
|
|
EnsureCombatFinishFormUseCaseBound();
|
|
EnsureRewardSelectFormUseCaseBound();
|
|
_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();
|
|
CloseRewardSelectForm();
|
|
_enemyManager.EndPhase();
|
|
_enemyManager.ResetCombatStats();
|
|
ResetRuntime();
|
|
_isFinishAsVictory = true;
|
|
|
|
_currentLevel = level;
|
|
_combatInRunResourceManager.InitializeForCombat(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}");
|
|
}
|
|
|
|
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)
|
|
{
|
|
_currentState?.OnUpdate(elapseSeconds, realElapseSeconds);
|
|
}
|
|
|
|
public void OnDestroy()
|
|
{
|
|
if (!_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentState?.OnExit();
|
|
_currentState?.OnDestroy();
|
|
_currentState = null;
|
|
CleanupAllCombatEntities();
|
|
CloseCombatFinishForm();
|
|
CloseRewardSelectForm();
|
|
_enemyManager.OnDestroy();
|
|
ResetRuntime();
|
|
_eventBridge.Unbind();
|
|
_combatFinishFormUseCase = null;
|
|
_rewardSelectFormUseCase = null;
|
|
|
|
_entity = null;
|
|
_initialized = false;
|
|
}
|
|
|
|
public bool TryEndCombatByPlayer()
|
|
{
|
|
if (_currentState is not CombatRunningPhaseState &&
|
|
_currentState is not CombatWaitingForPhaseEndState)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return _phaseLoopRuntime.TryRequestEndCombat();
|
|
}
|
|
|
|
public void OnEnemyReachedBase(DREnemy enemy)
|
|
{
|
|
if (!IsRunning)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int resolvedBaseDamage = enemy != null ? Mathf.Max(0, enemy.BaseDamage) : 0;
|
|
if (resolvedBaseDamage <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplyBaseDamage(resolvedBaseDamage);
|
|
}
|
|
|
|
public void OnEnemyDefeated(DREnemy enemy)
|
|
{
|
|
if (!IsRunning)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnemyDropResolveContext context = new(
|
|
enemy,
|
|
_phaseLoopRuntime.DisplayPhaseIndex,
|
|
ResolveCurrentThemeType());
|
|
EnemyDropResolveResult result = _enemyDropResolver.Resolve(context);
|
|
_combatInRunResourceManager.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
|
|
|
if (!result.ShouldRollOutGameItem)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_combatInRunResourceManager.TryRollOutGameItemDrop(
|
|
context.DisplayPhaseIndex,
|
|
context.ThemeType);
|
|
}
|
|
|
|
public bool OnCombatFinishReturnRequested()
|
|
{
|
|
if (_currentState is not CombatWaitingForReturnState waitingForReturnState)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
waitingForReturnState.RequestReturn();
|
|
return true;
|
|
}
|
|
|
|
public bool TryConsumeCoin(int coin)
|
|
{
|
|
return _combatInRunResourceManager.TryConsumeCoin(coin);
|
|
}
|
|
|
|
public void AddCoin(int coin)
|
|
{
|
|
_combatInRunResourceManager.AddCoin(coin);
|
|
}
|
|
|
|
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
|
|
{
|
|
return _combatInRunResourceManager.TryGetBuildTowerStats(buildIndex, out stats);
|
|
}
|
|
|
|
private void ResetRuntime()
|
|
{
|
|
_currentState = null;
|
|
_phaseBuffer.Clear();
|
|
_spawnEntriesByPhaseId.Clear();
|
|
_phaseLoopRuntime.Reset();
|
|
_loadSession.Reset();
|
|
_combatInRunResourceManager.Reset();
|
|
_settlementContext = null;
|
|
_currentLevel = null;
|
|
_isFinishAsVictory = true;
|
|
_isCompleted = false;
|
|
_nodeEnterFired = false;
|
|
}
|
|
|
|
private void CleanupAllCombatEntities()
|
|
{
|
|
_loadSession.Cleanup();
|
|
_enemyManager.CleanupTrackedEnemies();
|
|
}
|
|
|
|
private void EnsureCombatFinishFormUseCaseBound()
|
|
{
|
|
_combatFinishFormUseCase ??= new CombatFinishFormUseCase(this);
|
|
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase);
|
|
}
|
|
|
|
private void EnsureRewardSelectFormUseCaseBound()
|
|
{
|
|
_rewardSelectFormUseCase ??= new RewardSelectFormUseCase();
|
|
GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _rewardSelectFormUseCase);
|
|
}
|
|
|
|
private void ChangeState(CombatStateBase nextState)
|
|
{
|
|
if (ReferenceEquals(_currentState, nextState))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentState?.OnExit();
|
|
_currentState?.OnDestroy();
|
|
_currentState = nextState;
|
|
_currentState?.OnInit();
|
|
_currentState?.OnEnter();
|
|
}
|
|
|
|
private bool TryBeginNextPhase()
|
|
{
|
|
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 OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward)
|
|
{
|
|
if (_currentState is not CombatRewardSelectionState || _settlementContext == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_settlementFlowService.ApplySelectedReward(_settlementContext, selectedReward);
|
|
ChangeState(new CombatFinishFormState(this));
|
|
}
|
|
|
|
private void OnFullBaseHpRewardGiveUp()
|
|
{
|
|
if (_currentState is not CombatRewardSelectionState || _settlementContext == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ChangeState(new CombatFinishFormState(this));
|
|
}
|
|
|
|
private LevelThemeType ResolveCurrentThemeType()
|
|
{
|
|
if (_currentLevel != null)
|
|
{
|
|
return _currentLevel.LevelThemeType;
|
|
}
|
|
|
|
if (GameEntry.CombatNode != null)
|
|
{
|
|
return GameEntry.CombatNode.CurrentThemeType;
|
|
}
|
|
|
|
return LevelThemeType.None;
|
|
}
|
|
|
|
private int ApplyBaseDamage(int damage)
|
|
{
|
|
return _combatInRunResourceManager.ApplyBaseDamage(damage);
|
|
}
|
|
|
|
private int GetCurrentBaseHp()
|
|
{
|
|
return Mathf.Max(0, _combatInRunResourceManager.CurrentBaseHp);
|
|
}
|
|
|
|
private void CloseCombatFinishForm()
|
|
{
|
|
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
|
|
}
|
|
|
|
private void CloseRewardSelectForm()
|
|
{
|
|
GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm);
|
|
}
|
|
|
|
private void CompleteCombatAndNotify(bool succeeded)
|
|
{
|
|
_isCompleted = true;
|
|
_currentState = null;
|
|
_combatInRunResourceManager.MarkCombatEnded();
|
|
GameEntry.CombatNode?.OnCombatEndedByScheduler(succeeded);
|
|
}
|
|
|
|
private bool HandleStartFailure(string errorMessage)
|
|
{
|
|
Log.Warning("{0}", errorMessage);
|
|
_enemyManager.EndPhase();
|
|
CleanupAllCombatEntities();
|
|
CloseCombatFinishForm();
|
|
CloseRewardSelectForm();
|
|
ResetRuntime();
|
|
return false;
|
|
}
|
|
|
|
private void EnterFailureFallback(string errorMessage)
|
|
{
|
|
if (_currentState is CombatFailedState || _isCompleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ChangeState(new CombatFailedState(this, errorMessage, true));
|
|
}
|
|
|
|
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
|
|
|
|
private void GameOverByFailure()
|
|
{
|
|
CompleteCombatAndNotify(false);
|
|
}
|
|
|
|
}
|
|
}
|