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 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 enum SchedulerState : byte { Idle = 0, WaitingForLoading = 1, RunningPhase = 2, WaitingForFinishReturn = 3, Completed = 4, Failed = 5 } private readonly List _phaseBuffer = new(); private readonly Dictionary> _spawnEntriesByPhaseId = new(); private readonly EnemyManager _enemyManager = new(); private readonly PhaseLoopRuntime _phaseLoopRuntime = new(); private readonly CombatLoadSession _loadSession = new(); private readonly CombatEventBridge _eventBridge = new(); private readonly CombatResourceManager _combatResourceManager = new(); private EntityComponent _entity; private DRLevel _currentLevel; private CombatFinishFormUseCase _combatFinishFormUseCase; private RewardSelectFormUseCase _rewardSelectFormUseCase; private SchedulerState _state = SchedulerState.Idle; private bool _initialized; private bool _isFinishAsVictory = true; private int _pendingFinishDefeatedEnemyCount; private int _pendingFinishGainedGold; private BackpackInventoryData _pendingFinishRewardInventory; private bool _hasPendingFinishSettlement; public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase; public bool IsCompleted => _state == SchedulerState.Completed; public DRLevel CurrentLevel => _currentLevel; public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase; public MapEntity CurrentMap => _loadSession.CurrentMap; public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex; public int PhaseCount => _phaseLoopRuntime.PhaseCount; public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat; public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount; public int GainedCoin => _combatResourceManager.GainedCoin; public int GainedGold => _combatResourceManager.GainedGold; public void OnInit() { if (!_initialized) { _entity = GameEntry.Entity; _eventBridge.Bind( OnShowEntitySuccess, OnShowEntityFailure, OnHideEntityComplete, OnOpenUIFormSuccess, OnOpenUIFormFailure, OnCloseUIFormComplete); _enemyManager.OnInit(this); _loadSession.OnInit(_entity); EnsureCombatFinishFormUseCaseBound(); EnsureRewardSelectFormUseCaseBound(); _initialized = true; } ResetRuntime(); } public bool Start( DRLevel level, IReadOnlyList phases, IReadOnlyDictionary> 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(); _combatResourceManager.Reset(); _isFinishAsVictory = true; _currentLevel = level; for (int i = 0; i < phases.Count; i++) { DRLevelPhase phase = phases[i]; if (phase != null) { _phaseBuffer.Add(phase); } } if (spawnEntriesByPhaseId != null) { foreach (var pair in spawnEntriesByPhaseId) { _spawnEntriesByPhaseId[pair.Key] = pair.Value; } } _phaseLoopRuntime.SetPhases(_phaseBuffer); if (_phaseLoopRuntime.PhaseCount <= 0) { return HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data."); } if (!_loadSession.StartLoading(level, out string loadError)) { return HandleStartFailure($"CombatScheduler start failed. {loadError}"); } _state = SchedulerState.WaitingForLoading; Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, _phaseLoopRuntime.PhaseCount); return true; } public void OnUpdate(float elapseSeconds, float realElapseSeconds) { switch (_state) { case SchedulerState.WaitingForLoading: if (!_loadSession.IsReady) { return; } BeginNextPhase(); return; case SchedulerState.RunningPhase: UpdateCurrentPhase(elapseSeconds, realElapseSeconds); return; default: return; } } public void OnDestroy() { if (!_initialized) { return; } CleanupAllCombatEntities(); CloseCombatFinishForm(); CloseRewardSelectForm(); _enemyManager.OnDestroy(); ResetRuntime(); _eventBridge.Unbind(); _combatFinishFormUseCase = null; _rewardSelectFormUseCase = null; _entity = null; _initialized = false; } public bool TryEndCombatByPlayer() { if (_state != SchedulerState.RunningPhase) { return false; } if (!_phaseLoopRuntime.TryRequestEndCombat()) { return false; } EnterFinishFlow("Combat ended by player.", true); return true; } private void UpdateCurrentPhase(float elapseSeconds, float realElapseSeconds) { DRLevelPhase currentPhase = _phaseLoopRuntime.CurrentPhase; if (currentPhase == null) { EnterFailureFallback("CombatScheduler update failed. Current phase is null."); return; } _phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds); _enemyManager.OnUpdate(elapseSeconds, realElapseSeconds); if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted, _enemyManager.AliveEnemyCount)) { return; } CompleteCurrentPhase(); } private void CompleteCurrentPhase() { _enemyManager.EndPhase(); Log.Info( "CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.", _currentLevel != null ? _currentLevel.Id : 0, _phaseLoopRuntime.CurrentPhase != null ? _phaseLoopRuntime.CurrentPhase.Id : 0, _phaseLoopRuntime.CurrentPhaseElapsed); BeginNextPhase(); } private void BeginNextPhase() { if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase)) { EnterFinishFlow("Combat ended after loop completion.", true); return; } _spawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList 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) { if (_state != SchedulerState.RunningPhase) { return; } int resolvedBaseDamage = Mathf.Max(0, baseDamage); if (resolvedBaseDamage <= 0) { return; } CombatNodeComponent combatNode = GameEntry.CombatNode; if (combatNode == null) { return; } int currentBaseHp = combatNode.ApplyBaseDamage(resolvedBaseDamage); if (currentBaseHp > 0) { return; } EnterFinishFlow("Combat ended because base HP reached zero.", false); } private void ResetRuntime() { _phaseBuffer.Clear(); _spawnEntriesByPhaseId.Clear(); _phaseLoopRuntime.Reset(); _loadSession.Reset(); _combatResourceManager.Reset(); ClearPendingFinishContext(); _currentLevel = null; _isFinishAsVictory = true; _state = SchedulerState.Idle; } private void CleanupAllCombatEntities() { _loadSession.Cleanup(); _enemyManager.CleanupTrackedEnemies(); } private void EnsureCombatFinishFormUseCaseBound() { _combatFinishFormUseCase ??= new CombatFinishFormUseCase(this); GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase); } private void EnsureRewardSelectFormUseCaseBound() { _rewardSelectFormUseCase ??= new RewardSelectFormUseCase(); GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _rewardSelectFormUseCase); } private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory) { 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) { return; } BackpackInventoryData rewardInventory = _pendingFinishRewardInventory ?? new BackpackInventoryData(); int defeatedEnemyCount = _pendingFinishDefeatedEnemyCount; int gainedGold = _pendingFinishGainedGold; GameEntry.PlayerInventory?.MergeInventory(rewardInventory); OpenCombatFinishForm(defeatedEnemyCount, gainedGold, rewardInventory); ClearPendingFinishContext(); } private void ClearPendingFinishContext() { _pendingFinishDefeatedEnemyCount = 0; _pendingFinishGainedGold = 0; _pendingFinishRewardInventory = null; _hasPendingFinishSettlement = false; } 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 = 0; int maxBaseHp = 0; 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, _combatResourceManager.GainedGold) + levelRewardGold; int bonusGold = bonusRate > 0f ? Mathf.FloorToInt(goldForBonusCalculation * bonusRate) : 0; int settlementGold = levelRewardGold + bonusGold; _combatResourceManager.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 = 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) { 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 TryOpenFullBaseHpRewardSelect() { IReadOnlyList candidateItems = _combatResourceManager.RollSettlementRewardCandidates( _phaseLoopRuntime.DisplayPhaseIndex, ResolveCurrentThemeType(), RewardSelectDisplayCount); if (candidateItems == null || candidateItems.Count <= 0) { return false; } List rewardPool = new List(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) { 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) { return false; } GameEntry.UIRouter.OpenUI(UIFormType.RewardSelectForm, rawData); return true; } private void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward) { if (_pendingFinishRewardInventory != null && selectedReward?.SourceItem != null) { TryAppendRewardComponent(_pendingFinishRewardInventory, selectedReward.SourceItem); } CommitPendingSettlementAndOpenFinishForm(); } private void OnFullBaseHpRewardGiveUp() { CommitPendingSettlementAndOpenFinishForm(); } 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(), 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; } public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold) { _combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold); if (_state != SchedulerState.RunningPhase) { return; } _combatResourceManager.TryRollOutGameItemDrop( _phaseLoopRuntime.DisplayPhaseIndex, ResolveCurrentThemeType()); } private LevelThemeType ResolveCurrentThemeType() { if (_currentLevel != null) { return _currentLevel.LevelThemeType; } if (GameEntry.CombatNode != null) { return GameEntry.CombatNode.CurrentThemeType; } return LevelThemeType.None; } private void CloseCombatFinishForm() { GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm); } private void CloseRewardSelectForm() { GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm); } public bool OnCombatFinishReturnRequested() { 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; } private bool HandleStartFailure(string errorMessage) { Log.Warning("{0}", errorMessage); _state = SchedulerState.Failed; _enemyManager.EndPhase(); CleanupAllCombatEntities(); CloseCombatFinishForm(); CloseRewardSelectForm(); ResetRuntime(); return false; } private void EnterFailureFallback(string errorMessage) { if (_state == SchedulerState.Failed || _state == SchedulerState.Completed) { return; } _state = SchedulerState.Failed; Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0, errorMessage); _enemyManager.EndPhase(); CleanupAllCombatEntities(); CloseCombatFinishForm(); CloseRewardSelectForm(); GameEntry.CombatNode?.OnCombatEndedByScheduler(false); } private static int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount) { if (displayPhaseIndex <= 0 || phaseCount <= 0) { return 1; } int completedLoopCount = Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount); if (completedLoopCount >= 30) { return int.MaxValue; } return 1 << completedLoopCount; } #region Event Handlers private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args) { var status = _loadSession.HandleShowEntitySuccess(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { EnterFailureFallback(errorMessage); return; } if (status == CombatLoadSession.EventHandleStatus.Succeeded) { MapEntity map = _loadSession.CurrentMap; Log.Info( "Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.", _currentLevel != null ? _currentLevel.Id : 0, map.PathCells.Count, map.FoundationCells.Count, map.Spawners.Length, map.House != null ? map.House.name : "None"); } } private void OnShowEntityFailure(ShowEntityFailureEventArgs args) { var status = _loadSession.HandleShowEntityFailure(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { EnterFailureFallback(errorMessage); } } private void OnHideEntityComplete(HideEntityCompleteEventArgs args) { _loadSession.HandleHideEntityComplete(args); } private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args) { var status = _loadSession.HandleOpenUIFormSuccess(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { EnterFailureFallback(errorMessage); } } private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args) { var status = _loadSession.HandleOpenUIFormFailure(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { EnterFailureFallback(errorMessage); } } private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args) { _loadSession.HandleCloseUIFormComplete(args); } #endregion } }