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 _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 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 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(); _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 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 candidateItems = _combatInRunResourceManager.RollSettlementRewardCandidates( _phaseLoopRuntime.DisplayPhaseIndex, ResolveCurrentThemeType(), RewardSelectDisplayCount); if (candidateItems == null || candidateItems.Count <= 0) { settlementContext.ShouldOpenRewardSelection = false; 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) { 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(), 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; } } }