From 0ff04f02f430576b37a57eb74697397e919e0c9b Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Sat, 7 Mar 2026 11:21:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=20CombatScheduler=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=E5=88=B0=E7=8B=AC=E7=AB=8B=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CombatNode/CombatNodeComponent.cs | 2 - .../CombatScheduler/CombatScheduler.cs | 444 +++++++++--------- .../CombatScheduler/PhaseLoopRuntime.cs | 1 + .../CombatNode/CombatStates.meta | 3 + .../CombatStates/CombatFailedState.cs | 35 ++ .../CombatStates/CombatFailedState.cs.meta | 11 + .../CombatStates/CombatFinishFormState.cs | 28 ++ .../CombatFinishFormState.cs.meta | 11 + .../CombatStates/CombatLoadingState.cs | 25 + .../CombatStates/CombatLoadingState.cs.meta | 11 + .../CombatRewardSelectionState.cs | 31 ++ .../CombatRewardSelectionState.cs.meta | 11 + .../CombatStates/CombatRunningPhaseState.cs | 77 +++ .../CombatRunningPhaseState.cs.meta | 11 + .../CombatStates/CombatSettlementState.cs | 29 ++ .../CombatSettlementState.cs.meta | 11 + .../CombatStates/CombatStateBase.cs | 35 ++ .../CombatStates/CombatStateBase.cs.meta | 11 + .../CombatWaitingForPhaseEndState.cs | 40 ++ .../CombatWaitingForPhaseEndState.cs.meta | 11 + .../CombatWaitingForReturnState.cs | 35 ++ .../CombatWaitingForReturnState.cs.meta | 11 + .../CombatSelectUseCaseConfigurator.cs.meta | 11 + 23 files changed, 664 insertions(+), 231 deletions(-) create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFailedState.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFailedState.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFinishFormState.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFinishFormState.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatLoadingState.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatLoadingState.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRewardSelectionState.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRewardSelectionState.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRunningPhaseState.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRunningPhaseState.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatSettlementState.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatSettlementState.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForPhaseEndState.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForPhaseEndState.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForReturnState.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForReturnState.cs.meta create mode 100644 Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs.meta diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs index 4c81af6..a600298 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs @@ -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 } } - diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index 9f53a12..2e6d8a9 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -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 _phaseBuffer = new(); private readonly Dictionary> _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 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) + _combatResourceManager.TryRollOutGameItemDrop( + _phaseLoopRuntime.DisplayPhaseIndex, + ResolveCurrentThemeType()); + } + + public bool OnCombatFinishReturnRequested() + { + if (_currentState is not CombatWaitingForReturnState waitingForReturnState) { - return; + return false; } - EnterFinishFlow("Combat ended because base HP reached zero.", 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 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 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; + } + } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs index e548755..5a132e2 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs @@ -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; diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates.meta new file mode 100644 index 0000000..e4e2f51 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 76973ced080a4a839543fc9361b6b96b +timeCreated: 1772847309 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFailedState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFailedState.cs new file mode 100644 index 0000000..d2ae3d1 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFailedState.cs @@ -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(); + } + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFailedState.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFailedState.cs.meta new file mode 100644 index 0000000..61d6535 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFailedState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39d988ff69b018a49857384e3a3b6f20 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFinishFormState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFinishFormState.cs new file mode 100644 index 0000000..4a6aa1e --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFinishFormState.cs @@ -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)); + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFinishFormState.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFinishFormState.cs.meta new file mode 100644 index 0000000..b32ae60 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatFinishFormState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0d78014e1fa53434aa14628f83ed3669 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatLoadingState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatLoadingState.cs new file mode 100644 index 0000000..e6a6739 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatLoadingState.cs @@ -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(); + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatLoadingState.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatLoadingState.cs.meta new file mode 100644 index 0000000..7053ce9 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatLoadingState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f604bf4c97e462abf153804c1b0b55d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRewardSelectionState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRewardSelectionState.cs new file mode 100644 index 0000000..38d3013 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRewardSelectionState.cs @@ -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(); + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRewardSelectionState.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRewardSelectionState.cs.meta new file mode 100644 index 0000000..967b3c8 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRewardSelectionState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e087635834ae66943a6fc76e813ab1f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRunningPhaseState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRunningPhaseState.cs new file mode 100644 index 0000000..6e9511a --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRunningPhaseState.cs @@ -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 _spawnEntries; + + public CombatRunningPhaseState( + CombatScheduler scheduler, + DRLevelPhase phase, + IReadOnlyList 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(); + } + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRunningPhaseState.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRunningPhaseState.cs.meta new file mode 100644 index 0000000..c2f8201 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatRunningPhaseState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ea3f65c4dc4478ab57c6b8d7a5382b3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatSettlementState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatSettlementState.cs new file mode 100644 index 0000000..cbf533d --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatSettlementState.cs @@ -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)); + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatSettlementState.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatSettlementState.cs.meta new file mode 100644 index 0000000..90aa586 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatSettlementState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 61773c700734bfb4db073594ef2f97a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs new file mode 100644 index 0000000..57436c7 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs @@ -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() + { + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs.meta new file mode 100644 index 0000000..578b4d4 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9346a56a6d174c3fbc6889003134ddc6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForPhaseEndState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForPhaseEndState.cs new file mode 100644 index 0000000..f1f9c74 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForPhaseEndState.cs @@ -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(); + } + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForPhaseEndState.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForPhaseEndState.cs.meta new file mode 100644 index 0000000..a6087e2 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForPhaseEndState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 190af076775ad1b4c97a2baf09588bbf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForReturnState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForReturnState.cs new file mode 100644 index 0000000..f6cf190 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForReturnState.cs @@ -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); + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForReturnState.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForReturnState.cs.meta new file mode 100644 index 0000000..15bb00b --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatWaitingForReturnState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c86116fef176a840a38bd7c154a5deb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs.meta b/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs.meta new file mode 100644 index 0000000..0ab7569 --- /dev/null +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b957db4a74448154fbe3fbec36c33038 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: