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

766 lines
26 KiB
C#

using System;
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 const int RewardSelectDisplayCount = 3;
private const float FullBaseHpGoldBonusRate = 0.3f;
private const float HighBaseHpGoldBonusRate = 0.1f;
private const float HighBaseHpThreshold = 0.8f;
private const float MidBaseHpThreshold = 0.5f;
private const float LowBaseHpTowerEndurancePenalty = 10f;
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 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 SettlementContext _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 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();
_combatInRunResourceManager.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}");
}
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(int baseDamage)
{
if (!IsRunning)
{
return;
}
int resolvedBaseDamage = Mathf.Max(0, baseDamage);
if (resolvedBaseDamage <= 0)
{
return;
}
ApplyBaseDamage(resolvedBaseDamage);
}
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
{
_combatInRunResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold);
if (!IsRunning)
{
return;
}
_combatInRunResourceManager.TryRollOutGameItemDrop(
_phaseLoopRuntime.DisplayPhaseIndex,
ResolveCurrentThemeType());
}
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();
_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 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, _combatInRunResourceManager.GainedGold),
RewardInventory = _combatInRunResourceManager.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)
{
shouldOpenFullBaseHpRewardSelect = false;
if (!isVictory)
{
return;
}
int levelRewardGold = _currentLevel != null ? Mathf.Max(0, _currentLevel.RewardGold) : 0;
int currentBaseHp;
int maxBaseHp;
float bonusRate = 0f;
bool appliedLowBaseHpPenalty = false;
ResolveBaseHpSnapshot(out currentBaseHp, out maxBaseHp);
if (maxBaseHp > 0 && currentBaseHp >= maxBaseHp)
{
bonusRate = FullBaseHpGoldBonusRate;
shouldOpenFullBaseHpRewardSelect = true;
}
else if (maxBaseHp > 0)
{
float hpRate = (float)Mathf.Clamp(currentBaseHp, 0, maxBaseHp) / maxBaseHp;
if (hpRate >= HighBaseHpThreshold)
{
bonusRate = HighBaseHpGoldBonusRate;
}
else if (hpRate < MidBaseHpThreshold)
{
appliedLowBaseHpPenalty = ApplyLowBaseHpPenalty();
}
}
int goldForBonusCalculation = Mathf.Max(0, _combatInRunResourceManager.GainedGold) + levelRewardGold;
int bonusGold = bonusRate > 0f ? Mathf.FloorToInt(goldForBonusCalculation * bonusRate) : 0;
int settlementGold = levelRewardGold + bonusGold;
_combatInRunResourceManager.AddSettlementGold(settlementGold);
Log.Info(
"Combat settlement resolved. BaseHp={0}/{1}, LevelReward={2}, BonusRate={3:P0}, BonusGold={4}, FullHpRewardSelect={5}, LowHpPenalty={6}.",
currentBaseHp,
maxBaseHp,
levelRewardGold,
bonusRate,
bonusGold,
shouldOpenFullBaseHpRewardSelect,
appliedLowBaseHpPenalty);
}
private void ResolveBaseHpSnapshot(out int currentBaseHp, out int maxBaseHp)
{
currentBaseHp = Mathf.Max(0, GetCurrentBaseHp());
maxBaseHp = _currentLevel != null ? Mathf.Max(0, _currentLevel.BaseHp) : 0;
if (maxBaseHp > 0)
{
currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp);
}
}
private bool ApplyLowBaseHpPenalty()
{
PlayerInventoryComponent inventory = GameEntry.PlayerInventory;
if (inventory == null)
{
return false;
}
int affectedTowerCount = inventory.ReduceAllTowerEndurance(LowBaseHpTowerEndurancePenalty);
return affectedTowerCount > 0;
}
private bool TryPrepareRewardSelection(SettlementContext settlementContext)
{
IReadOnlyList<TowerCompItemData> candidateItems = _combatInRunResourceManager.RollSettlementRewardCandidates(
_phaseLoopRuntime.DisplayPhaseIndex,
ResolveCurrentThemeType(),
RewardSelectDisplayCount);
if (candidateItems == null || candidateItems.Count <= 0)
{
settlementContext.ShouldOpenRewardSelection = false;
return false;
}
List<RewardSelectItemRawData> rewardPool = new List<RewardSelectItemRawData>(candidateItems.Count);
for (int i = 0; i < candidateItems.Count; i++)
{
TowerCompItemData item = candidateItems[i];
if (item == null)
{
continue;
}
rewardPool.Add(BuildRewardSelectRawData(item));
}
if (rewardPool.Count <= 0)
{
settlementContext.ShouldOpenRewardSelection = false;
return false;
}
EnsureRewardSelectFormUseCaseBound();
_rewardSelectFormUseCase.SetCallbacks(OnFullBaseHpRewardSelected, OnFullBaseHpRewardGiveUp);
_rewardSelectFormUseCase.ConfigureRewardPool(
rewardPool,
displayCount: RewardSelectDisplayCount,
refreshCost: 0,
allowRefreshOnce: false,
allowGiveUp: false,
tipText: "基地满血奖励:请选择 1 个组件");
RewardSelectFormRawData rawData = _rewardSelectFormUseCase.CreateInitialModel();
if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0)
{
settlementContext.ShouldOpenRewardSelection = false;
return false;
}
GameEntry.UIRouter.OpenUI(UIFormType.RewardSelectForm, rawData);
return true;
}
private void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward)
{
if (_currentState is not CombatRewardSelectionState || _settlementContext == null)
{
return;
}
if (_settlementContext.RewardInventory != null && selectedReward?.SourceItem != null)
{
TryAppendRewardComponent(_settlementContext.RewardInventory, selectedReward.SourceItem);
}
ChangeState(new CombatFinishFormState(this));
}
private void OnFullBaseHpRewardGiveUp()
{
if (_currentState is not CombatRewardSelectionState || _settlementContext == null)
{
return;
}
ChangeState(new CombatFinishFormState(this));
}
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)
{
if (rewardInventory == null || selectedItem == null)
{
return false;
}
if (selectedItem is MuzzleCompItemData muzzleComp)
{
rewardInventory.MuzzleComponents.Add(muzzleComp);
return true;
}
if (selectedItem is BearingCompItemData bearingComp)
{
rewardInventory.BearingComponents.Add(bearingComp);
return true;
}
if (selectedItem is BaseCompItemData baseComp)
{
rewardInventory.BaseComponents.Add(baseComp);
return true;
}
return false;
}
private static RewardSelectItemRawData BuildRewardSelectRawData(TowerCompItemData item)
{
return new RewardSelectItemRawData
{
RewardId = item.InstanceId,
SlotType = item.SlotType,
Title = item.Name,
TypeText = BuildRewardTypeText(item.SlotType),
Description = BuildRewardDescription(item),
Rarity = item.Rarity,
Tags = item.Tags != null ? (TagType[])item.Tags.Clone() : Array.Empty<TagType>(),
Icon = null,
IsSelectable = true,
SourceItem = item
};
}
private static string BuildRewardTypeText(TowerCompSlotType slotType)
{
return slotType switch
{
TowerCompSlotType.Muzzle => "Muzzle Component",
TowerCompSlotType.Bearing => "Bearing Component",
TowerCompSlotType.Base => "Base Component",
TowerCompSlotType.Accessory => "Accessory",
_ => "Component"
};
}
private static string BuildRewardDescription(TowerCompItemData item)
{
if (item is MuzzleCompItemData muzzle)
{
int damage = muzzle.AttackDamage != null && muzzle.AttackDamage.Length > 0 ? muzzle.AttackDamage[0] : 0;
return $"Damage: {damage}, Spread: {muzzle.DamageRandomRate:P0}";
}
if (item is BearingCompItemData bearing)
{
float range = bearing.AttackRange != null && bearing.AttackRange.Length > 0 ? bearing.AttackRange[0] : 0f;
float rotateSpeed = bearing.RotateSpeed != null && bearing.RotateSpeed.Length > 0 ? bearing.RotateSpeed[0] : 0f;
return $"Range: {range:0.##}, Rotate Speed: {rotateSpeed:0.##}";
}
if (item is BaseCompItemData baseComp)
{
float attackSpeed = baseComp.AttackSpeed != null && baseComp.AttackSpeed.Length > 0 ? baseComp.AttackSpeed[0] : 0f;
return $"Attack Speed: {attackSpeed:0.##}, Property: {baseComp.AttackPropertyType}";
}
return string.Empty;
}
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)
{
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);
}
private void CloseRewardSelectForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm);
}
private void CompleteCombatAndNotify(bool succeeded)
{
_isCompleted = true;
_currentState = null;
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);
}
private sealed class SettlementContext
{
public int DefeatedEnemyCount;
public int GainedGold;
public BackpackInventoryData RewardInventory;
public bool ShouldOpenRewardSelection;
public bool IsCommitted;
public string Reason;
}
}
}