refactor & fix

- 将 `CombatScheduler` 的职责分散到 3 个类中
    - `PhaseLoopRuntime`:负责关卡各波次的调度
    - `CombatLoadSession`:负责地图与 UI 等资源的加载
    - `CombatEventBridge`:负责相关事件的处理
- 统一了战斗的命名为 `Combat`
- 调整了各个地图的坐标,使其位于摄像头中心
- 新增了 CombatInfoForm 展示战斗内的信息
- 补充了循环执行 LevelPhase,手动结束战斗的逻辑
This commit is contained in:
SepComet
2026-03-01 13:15:53 +08:00
parent 26dc1a5600
commit 92cca14503
99 changed files with 2418 additions and 243 deletions
@@ -0,0 +1,151 @@
using System;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatEventBridge
{
private Action<ShowEntitySuccessEventArgs> _onShowEntitySuccess;
private Action<ShowEntityFailureEventArgs> _onShowEntityFailure;
private Action<HideEntityCompleteEventArgs> _onHideEntityComplete;
private Action<OpenUIFormSuccessEventArgs> _onOpenUIFormSuccess;
private Action<OpenUIFormFailureEventArgs> _onOpenUIFormFailure;
private Action<CloseUIFormCompleteEventArgs> _onCloseUIFormComplete;
private bool _isEntityEventSubscribed;
private bool _isUIEventSubscribed;
public void Bind(
Action<ShowEntitySuccessEventArgs> onShowEntitySuccess,
Action<ShowEntityFailureEventArgs> onShowEntityFailure,
Action<HideEntityCompleteEventArgs> onHideEntityComplete,
Action<OpenUIFormSuccessEventArgs> onOpenUIFormSuccess,
Action<OpenUIFormFailureEventArgs> onOpenUIFormFailure,
Action<CloseUIFormCompleteEventArgs> onCloseUIFormComplete)
{
_onShowEntitySuccess = onShowEntitySuccess;
_onShowEntityFailure = onShowEntityFailure;
_onHideEntityComplete = onHideEntityComplete;
_onOpenUIFormSuccess = onOpenUIFormSuccess;
_onOpenUIFormFailure = onOpenUIFormFailure;
_onCloseUIFormComplete = onCloseUIFormComplete;
EnsureEntityEventSubscribed();
EnsureUIEventSubscribed();
}
public void Unbind()
{
UnsubscribeEntityEvents();
UnsubscribeUIEvents();
_onShowEntitySuccess = null;
_onShowEntityFailure = null;
_onHideEntityComplete = null;
_onOpenUIFormSuccess = null;
_onOpenUIFormFailure = null;
_onCloseUIFormComplete = null;
}
private void EnsureEntityEventSubscribed()
{
if (_isEntityEventSubscribed)
{
return;
}
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_isEntityEventSubscribed = true;
}
private void EnsureUIEventSubscribed()
{
if (_isUIEventSubscribed)
{
return;
}
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnOpenUIFormSuccess);
GameEntry.Event.Subscribe(OpenUIFormFailureEventArgs.EventId, OnOpenUIFormFailure);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnCloseUIFormComplete);
_isUIEventSubscribed = true;
}
private void UnsubscribeEntityEvents()
{
if (!_isEntityEventSubscribed)
{
return;
}
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_isEntityEventSubscribed = false;
}
private void UnsubscribeUIEvents()
{
if (!_isUIEventSubscribed)
{
return;
}
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnOpenUIFormSuccess);
GameEntry.Event.Unsubscribe(OpenUIFormFailureEventArgs.EventId, OnOpenUIFormFailure);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnCloseUIFormComplete);
_isUIEventSubscribed = false;
}
private void OnShowEntitySuccess(object sender, GameEventArgs e)
{
if (e is ShowEntitySuccessEventArgs args)
{
_onShowEntitySuccess?.Invoke(args);
}
}
private void OnShowEntityFailure(object sender, GameEventArgs e)
{
if (e is ShowEntityFailureEventArgs args)
{
_onShowEntityFailure?.Invoke(args);
}
}
private void OnHideEntityComplete(object sender, GameEventArgs e)
{
if (e is HideEntityCompleteEventArgs args)
{
_onHideEntityComplete?.Invoke(args);
}
}
private void OnOpenUIFormSuccess(object sender, GameEventArgs e)
{
if (e is OpenUIFormSuccessEventArgs args)
{
_onOpenUIFormSuccess?.Invoke(args);
}
}
private void OnOpenUIFormFailure(object sender, GameEventArgs e)
{
if (e is OpenUIFormFailureEventArgs args)
{
_onOpenUIFormFailure?.Invoke(args);
}
}
private void OnCloseUIFormComplete(object sender, GameEventArgs e)
{
if (e is CloseUIFormCompleteEventArgs args)
{
_onCloseUIFormComplete?.Invoke(args);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b8f34e0186c449fd8ff7a9d22c8aeb52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,276 @@
using GameFramework.Resource;
using GeometryTD.CustomUtility;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.Entity.EntityData;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatLoadSession
{
internal enum EventHandleStatus : byte
{
Ignored = 0,
Succeeded = 1,
Failed = 2
}
private EntityComponent _entity;
private CombatInfoFormUseCase _useCase;
private int _loadingMapEntityId;
private int _loadedMapEntityId;
private int? _loadingCombatInfoFormSerialId;
private bool _isCombatInfoFormReady;
private MapEntity _currentMap;
public MapEntity CurrentMap => _currentMap;
public bool IsReady => _currentMap != null && _isCombatInfoFormReady;
public void OnInit(EntityComponent entityComponent)
{
_entity = entityComponent;
Reset();
}
public void Reset()
{
_loadingMapEntityId = 0;
_loadedMapEntityId = 0;
_loadingCombatInfoFormSerialId = null;
_isCombatInfoFormReady = false;
_currentMap = null;
}
public bool StartLoading(DRLevel level, out string errorMessage)
{
errorMessage = null;
if (_entity == null)
{
errorMessage = "CombatLoadSession start failed. Entity component is null.";
return false;
}
if (!TryShowMap(level, out errorMessage))
{
return false;
}
if (!TryOpenCombatInfoForm(out errorMessage))
{
return false;
}
return true;
}
public void RefreshCombatInfoForm(DRLevel currentLevel)
{
if (currentLevel == null)
{
return;
}
GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
}
public void Cleanup()
{
if (_entity == null)
{
return;
}
if (_loadingMapEntityId != 0 && _entity.IsLoadingEntity(_loadingMapEntityId))
{
_entity.HideEntity(_loadingMapEntityId);
}
if (_currentMap != null)
{
_entity.HideEntity(_currentMap);
}
else if (_loadedMapEntityId != 0)
{
UnityGameFramework.Runtime.Entity loadedMapEntity = _entity.GetEntity(_loadedMapEntityId);
if (loadedMapEntity != null)
{
_entity.HideEntity(loadedMapEntity);
}
}
CloseCombatInfoForm();
}
public EventHandleStatus HandleShowEntitySuccess(ShowEntitySuccessEventArgs args, out string errorMessage)
{
errorMessage = null;
if (args == null)
{
return EventHandleStatus.Ignored;
}
if (args.EntityLogicType != typeof(MapEntity) || args.Entity == null || args.Entity.Id != _loadingMapEntityId)
{
return EventHandleStatus.Ignored;
}
_loadedMapEntityId = _loadingMapEntityId;
_loadingMapEntityId = 0;
_currentMap = args.Entity.Logic as MapEntity;
if (_currentMap == null)
{
errorMessage = $"Loaded map entity logic is invalid. EntityId={args.Entity.Id}.";
return EventHandleStatus.Failed;
}
return EventHandleStatus.Succeeded;
}
public EventHandleStatus HandleShowEntityFailure(ShowEntityFailureEventArgs args, out string errorMessage)
{
errorMessage = null;
if (args == null)
{
return EventHandleStatus.Ignored;
}
if (args.EntityLogicType != typeof(MapEntity) || args.EntityId != _loadingMapEntityId)
{
return EventHandleStatus.Ignored;
}
_loadingMapEntityId = 0;
_currentMap = null;
errorMessage = $"Map load failed. Asset='{args.EntityAssetName}', Error='{args.ErrorMessage}'.";
return EventHandleStatus.Failed;
}
public void HandleHideEntityComplete(HideEntityCompleteEventArgs args)
{
if (args == null)
{
return;
}
if (args.EntityId == _loadedMapEntityId)
{
_loadedMapEntityId = 0;
_currentMap = null;
}
if (args.EntityId == _loadingMapEntityId)
{
_loadingMapEntityId = 0;
}
}
public EventHandleStatus HandleOpenUIFormSuccess(OpenUIFormSuccessEventArgs args, out string errorMessage)
{
errorMessage = null;
if (args == null || !_loadingCombatInfoFormSerialId.HasValue || args.UIForm == null)
{
return EventHandleStatus.Ignored;
}
if (args.UIForm.SerialId != _loadingCombatInfoFormSerialId.Value)
{
return EventHandleStatus.Ignored;
}
_isCombatInfoFormReady = args.UIForm.Logic is CombatInfoForm;
if (!_isCombatInfoFormReady)
{
errorMessage = "CombatInfoForm open success but form logic type is invalid.";
return EventHandleStatus.Failed;
}
return EventHandleStatus.Succeeded;
}
public EventHandleStatus HandleOpenUIFormFailure(OpenUIFormFailureEventArgs args, out string errorMessage)
{
errorMessage = null;
if (args == null || !_loadingCombatInfoFormSerialId.HasValue)
{
return EventHandleStatus.Ignored;
}
if (args.SerialId != _loadingCombatInfoFormSerialId.Value)
{
return EventHandleStatus.Ignored;
}
_isCombatInfoFormReady = false;
errorMessage = $"CombatInfoForm load failed. Asset='{args.UIFormAssetName}', Error='{args.ErrorMessage}'.";
return EventHandleStatus.Failed;
}
public void HandleCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
{
if (args == null || !_loadingCombatInfoFormSerialId.HasValue)
{
return;
}
if (args.SerialId == _loadingCombatInfoFormSerialId.Value)
{
_loadingCombatInfoFormSerialId = null;
_isCombatInfoFormReady = false;
}
}
private bool TryShowMap(DRLevel level, out string errorMessage)
{
errorMessage = null;
if (level == null)
{
errorMessage = "CombatLoadSession map load failed. Level is null.";
return false;
}
string mapAssetName = level.Id.ToString();
string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName);
if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist)
{
errorMessage = $"CombatLoadSession map asset not found. Level='{level.Id}', Asset='{mapAssetPath}'.";
return false;
}
_loadingMapEntityId = _entity.GenerateSerialId();
_entity.ShowMap(new MapData(_loadingMapEntityId, level.Id, Vector3.zero), mapAssetName);
return true;
}
private bool TryOpenCombatInfoForm(out string errorMessage)
{
errorMessage = null;
if (_useCase == null)
{
_useCase = new CombatInfoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _useCase);
}
int? serialId = GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
if (!serialId.HasValue)
{
errorMessage = "CombatLoadSession failed to open CombatInfoForm.";
return false;
}
_loadingCombatInfoFormSerialId = serialId.Value;
_isCombatInfoFormReady = GameEntry.UI.HasUIForm(serialId.Value);
return true;
}
private void CloseCombatInfoForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.CombatInfoForm);
_loadingCombatInfoFormSerialId = null;
_isCombatInfoFormReady = false;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36fe6a0f3baf4f69a1b131cd9b6fd223
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -26,16 +26,46 @@ namespace GeometryTD.CustomComponent
private readonly CombatScheduler _combatScheduler = new CombatScheduler();
private bool _runtimeInitialized;
private int _currentCoin;
public LevelThemeType CurrentThemeType { get; private set; }
public DRLevel CurrentLevel { get; private set; }
public int CurrentCoin => _currentCoin;
public bool CanEndCombat => _combatScheduler.CanEndCombat;
public int CurrentLevelPhaseCount
{
get
{
if (CurrentLevel == null)
{
return 0;
}
if (!_phasesByLevelId.TryGetValue(CurrentLevel.Id, out List<DRLevelPhase> phases) || phases == null)
{
return 0;
}
return phases.Count;
}
}
public int CurrentPhaseIndex
{
get
{
return _combatScheduler.DisplayPhaseIndex;
}
}
public void OnInit(LevelThemeType themeType)
{
ShutdownBattleRuntime();
ShutdownCombatRuntime();
CurrentThemeType = themeType;
CurrentLevel = null;
_currentCoin = 0;
_levelsById.Clear();
_phasesByLevelId.Clear();
_spawnEntriesByPhaseId.Clear();
@@ -129,12 +159,12 @@ namespace GeometryTD.CustomComponent
CountPhases(),
CountEntries());
EnsureBattleRuntimeInitialized();
EnsureCombatRuntimeInitialized();
}
public void StartCombat()
{
if (!EnsureBattleRuntimeInitialized())
if (!EnsureCombatRuntimeInitialized())
{
Log.Warning("CombatNodeComponent start failed. Missing scheduler runtime.");
return;
@@ -169,15 +199,22 @@ namespace GeometryTD.CustomComponent
}
CurrentLevel = selectedLevel;
_currentCoin = Mathf.Max(0, selectedLevel.StartCoin);
_combatScheduler.Start(selectedLevel, phaseList, _selectedSpawnEntriesByPhaseId);
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
}
public void EndCombat()
{
_currentCoin = 0;
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
}
public bool TryEndCombatByPlayer()
{
return _combatScheduler.TryEndCombatByPlayer();
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_runtimeInitialized)
@@ -191,7 +228,8 @@ namespace GeometryTD.CustomComponent
public void OnShutdown()
{
CurrentLevel = null;
ShutdownBattleRuntime();
_currentCoin = 0;
ShutdownCombatRuntime();
}
private void OnDestroy()
@@ -199,7 +237,7 @@ namespace GeometryTD.CustomComponent
OnShutdown();
}
private bool EnsureBattleRuntimeInitialized()
private bool EnsureCombatRuntimeInitialized()
{
if (_runtimeInitialized)
{
@@ -211,7 +249,7 @@ namespace GeometryTD.CustomComponent
return true;
}
private void ShutdownBattleRuntime()
private void ShutdownCombatRuntime()
{
if (!_runtimeInitialized)
{
@@ -257,4 +295,4 @@ namespace GeometryTD.CustomComponent
return count;
}
}
}
}
@@ -1,13 +1,6 @@
using System.Collections.Generic;
using System.Globalization;
using GameFramework.Event;
using GameFramework.Resource;
using GeometryTD.CustomUtility;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
@@ -17,43 +10,46 @@ namespace GeometryTD.CustomComponent
private enum SchedulerState : byte
{
Idle = 0,
WaitingForMap = 1,
WaitingForLoading = 1,
RunningPhase = 2,
Completed = 3,
Failed = 4
}
private readonly List<DRLevelPhase> _phaseBuffer = new();
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
private readonly EnemyManager _enemyManager = new EnemyManager();
private readonly EnemyManager _enemyManager = new();
private readonly PhaseLoopRuntime _phaseLoopRuntime = new();
private readonly CombatLoadSession _loadSession = new();
private readonly CombatEventBridge _eventBridge = new();
private EntityComponent _entity;
private DRLevel _currentLevel;
private DRLevelPhase _currentPhase;
private int _currentPhaseIndex;
private float _currentPhaseElapsed;
private SchedulerState _state = SchedulerState.Idle;
private int _loadingMapEntityId;
private int _loadedMapEntityId;
private bool _initialized;
private bool _isEntityEventSubscribed;
private MapEntity _currentMap;
public bool IsRunning => _state == SchedulerState.WaitingForMap || _state == SchedulerState.RunningPhase;
public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase;
public bool IsCompleted => _state == SchedulerState.Completed;
public DRLevel CurrentLevel => _currentLevel;
public DRLevelPhase CurrentPhase => _currentPhase;
public MapEntity CurrentMap => _currentMap;
public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase;
public MapEntity CurrentMap => _loadSession.CurrentMap;
public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex;
public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat;
public void OnInit()
{
if (!_initialized)
{
_entity = GameEntry.Entity;
EnsureEntityEventSubscribed();
_eventBridge.Bind(
OnShowEntitySuccess,
OnShowEntityFailure,
OnHideEntityComplete,
OnOpenUIFormSuccess,
OnOpenUIFormFailure,
OnCloseUIFormComplete);
_enemyManager.OnInit(this);
_loadSession.OnInit(_entity);
_initialized = true;
}
@@ -79,14 +75,18 @@ namespace GeometryTD.CustomComponent
return;
}
CleanupBattleEntities();
CleanupCombatEntities();
_enemyManager.EndPhase();
ResetRuntime();
_currentLevel = level;
foreach (var phase in phases)
for (int i = 0; i < phases.Count; i++)
{
_phaseBuffer.Add(phase);
DRLevelPhase phase = phases[i];
if (phase != null)
{
_phaseBuffer.Add(phase);
}
}
if (spawnEntriesByPhaseId != null)
@@ -97,33 +97,31 @@ namespace GeometryTD.CustomComponent
}
}
if (_phaseBuffer.Count <= 0)
_phaseLoopRuntime.SetPhases(_phaseBuffer);
if (_phaseLoopRuntime.PhaseCount <= 0)
{
_state = SchedulerState.Failed;
Log.Warning("CombatScheduler start failed. Level '{0}' has no phase data.", level.Id);
return;
}
if (!TryShowMap(level))
if (!_loadSession.StartLoading(level, out string loadError))
{
_state = SchedulerState.Failed;
Log.Warning("CombatScheduler start failed. {0}", loadError);
return;
}
_currentPhase = null;
_currentPhaseIndex = -1;
_currentPhaseElapsed = 0f;
_state = SchedulerState.WaitingForMap;
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, _phaseBuffer.Count);
_state = SchedulerState.WaitingForLoading;
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, _phaseLoopRuntime.PhaseCount);
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
switch (_state)
{
case SchedulerState.WaitingForMap:
if (_currentMap == null)
case SchedulerState.WaitingForLoading:
if (!_loadSession.IsReady)
{
return;
}
@@ -145,51 +143,49 @@ namespace GeometryTD.CustomComponent
return;
}
CleanupBattleEntities();
CleanupCombatEntities();
_enemyManager.OnDestroy();
ResetRuntime();
UnsubscribeEntityEvents();
_eventBridge.Unbind();
_entity = null;
_initialized = false;
}
private bool TryShowMap(DRLevel level)
public bool TryEndCombatByPlayer()
{
string mapAssetName = level.Id.ToString();
string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName);
if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist)
if (_state == SchedulerState.Completed || _state == SchedulerState.Failed || _state == SchedulerState.Idle)
{
Log.Warning(
"CombatScheduler start failed. Level '{0}' map asset not found: '{1}'.",
level.Id,
mapAssetPath);
return false;
}
_loadingMapEntityId = _entity.GenerateSerialId();
_entity.ShowMap(new MapData(_loadingMapEntityId, level.Id, Vector3.zero), mapAssetName);
if (!_phaseLoopRuntime.TryRequestEndCombat())
{
return false;
}
Log.Info(
"CombatScheduler loading map. Level={0}, Asset='{1}', EntityId={2}.",
level.Id,
mapAssetPath,
_loadingMapEntityId);
FinishCombat("Combat ended by player.");
return true;
}
private void RefreshCombatInfoForm()
{
_loadSession.RefreshCombatInfoForm(_currentLevel);
}
private void UpdateCurrentPhase(float elapseSeconds, float realElapseSeconds)
{
if (_currentPhase == null)
DRLevelPhase currentPhase = _phaseLoopRuntime.CurrentPhase;
if (currentPhase == null)
{
_state = SchedulerState.Failed;
Log.Warning("CombatScheduler update failed. Current phase is null.");
return;
}
_currentPhaseElapsed += elapseSeconds;
_phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
_enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
if (!ShouldEndCurrentPhase())
if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted, _enemyManager.AliveEnemyCount))
{
return;
}
@@ -197,229 +193,134 @@ namespace GeometryTD.CustomComponent
CompleteCurrentPhase();
}
private bool ShouldEndCurrentPhase()
{
switch (_currentPhase.EndType)
{
case PhaseEndType.TimeElapsed:
return _currentPhaseElapsed >= ResolveTimeElapsedThreshold(_currentPhase);
case PhaseEndType.EnemiesCleared:
case PhaseEndType.BossDead:
return _enemyManager.IsPhaseSpawnCompleted && _enemyManager.AliveEnemyCount <= 0;
case PhaseEndType.None:
default:
if (_currentPhase.DurationSeconds > 0 && _currentPhaseElapsed >= _currentPhase.DurationSeconds)
{
return true;
}
return _enemyManager.IsPhaseSpawnCompleted && _enemyManager.AliveEnemyCount <= 0;
}
}
private float ResolveTimeElapsedThreshold(DRLevelPhase phase)
{
if (!string.IsNullOrWhiteSpace(phase.EndParam) &&
float.TryParse(phase.EndParam, NumberStyles.Float, CultureInfo.InvariantCulture,
out float parsedSeconds))
{
return parsedSeconds;
}
if (phase.DurationSeconds > 0)
{
return phase.DurationSeconds;
}
return 0f;
}
private void CompleteCurrentPhase()
{
_enemyManager.EndPhase();
Log.Info(
"CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.",
_currentLevel != null ? _currentLevel.Id : 0,
_currentPhase != null ? _currentPhase.Id : 0,
_currentPhaseElapsed);
_phaseLoopRuntime.CurrentPhase != null ? _phaseLoopRuntime.CurrentPhase.Id : 0,
_phaseLoopRuntime.CurrentPhaseElapsed);
BeginNextPhase();
}
private void BeginNextPhase()
{
_currentPhaseIndex++;
if (_currentPhaseIndex >= _phaseBuffer.Count)
if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
{
_state = SchedulerState.Completed;
_currentPhase = null;
if (_currentMap != null)
{
_entity.HideEntity(_currentMap);
}
Log.Info("CombatScheduler level completed. Level={0}.", _currentLevel != null ? _currentLevel.Id : 0);
GameEntry.CombatNode.EndCombat();
FinishCombat("Combat ended after loop completion.");
return;
}
_currentPhase = _phaseBuffer[_currentPhaseIndex];
_currentPhaseElapsed = 0f;
RefreshCombatInfoForm();
IReadOnlyList<DRLevelSpawnEntry> spawnEntries = null;
_spawnEntriesByPhaseId.TryGetValue(_currentPhase.Id, out spawnEntries);
_enemyManager.BeginPhase(_currentPhase, spawnEntries);
_spawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries);
_enemyManager.BeginPhase(nextPhase, spawnEntries);
_state = SchedulerState.RunningPhase;
Log.Info(
"CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.",
_currentLevel != null ? _currentLevel.Id : 0,
_currentPhase.Id,
_currentPhase.EndType,
_phaseLoopRuntime.DisplayPhaseIndex,
nextPhase.EndType,
spawnEntries != null ? spawnEntries.Count : 0);
}
private void EnsureEntityEventSubscribed()
private void FinishCombat(string reason)
{
if (_isEntityEventSubscribed)
{
return;
}
_state = SchedulerState.Completed;
CleanupCombatEntities();
_enemyManager.EndPhase();
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_isEntityEventSubscribed = true;
}
private void UnsubscribeEntityEvents()
{
if (!_isEntityEventSubscribed)
{
return;
}
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_isEntityEventSubscribed = false;
Log.Info(
"CombatScheduler level completed. Level={0}. Reason={1}",
_currentLevel != null ? _currentLevel.Id : 0,
reason);
GameEntry.CombatNode.EndCombat();
}
private void ResetRuntime()
{
_phaseBuffer.Clear();
_spawnEntriesByPhaseId.Clear();
_phaseLoopRuntime.Reset();
_loadSession.Reset();
_currentLevel = null;
_currentPhase = null;
_currentPhaseIndex = -1;
_currentPhaseElapsed = 0f;
_loadingMapEntityId = 0;
_loadedMapEntityId = 0;
_currentMap = null;
_state = SchedulerState.Idle;
}
private void CleanupBattleEntities()
private void CleanupCombatEntities()
{
if (_entity == null)
{
return;
}
if (_loadingMapEntityId != 0 && _entity.IsLoadingEntity(_loadingMapEntityId))
{
_entity.HideEntity(_loadingMapEntityId);
}
if (_currentMap != null)
{
_entity.HideEntity(_currentMap);
}
else if (_loadedMapEntityId != 0)
{
UnityGameFramework.Runtime.Entity loadedMapEntity = _entity.GetEntity(_loadedMapEntityId);
if (loadedMapEntity != null)
{
_entity.HideEntity(loadedMapEntity);
}
}
_loadSession.Cleanup();
_enemyManager.CleanupTrackedEnemies();
}
#region Event Handlers
private void OnShowEntitySuccess(object sender, GameEventArgs e)
private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args)
{
if (!(e is ShowEntitySuccessEventArgs ne))
{
return;
}
if (ne.EntityLogicType != typeof(MapEntity) || ne.Entity.Id != _loadingMapEntityId)
{
return;
}
_loadedMapEntityId = _loadingMapEntityId;
_loadingMapEntityId = 0;
_currentMap = ne.Entity.Logic as MapEntity;
if (_currentMap == null)
var status = _loadSession.HandleShowEntitySuccess(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_state = SchedulerState.Failed;
Log.Error("Loaded map entity logic is invalid. EntityId={0}.", ne.Entity.Id);
Log.Error("CombatScheduler failed. {0}", errorMessage);
return;
}
Log.Info(
"Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.",
_currentLevel != null ? _currentLevel.Id : 0,
_currentMap.PathCells.Count,
_currentMap.FoundationCells.Count,
_currentMap.Spawners.Length,
_currentMap.House != null ? _currentMap.House.name : "None");
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(object sender, GameEventArgs e)
private void OnShowEntityFailure(ShowEntityFailureEventArgs args)
{
if (!(e is ShowEntityFailureEventArgs ne))
var status = _loadSession.HandleShowEntityFailure(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
return;
_state = SchedulerState.Failed;
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0, errorMessage);
}
if (ne.EntityLogicType != typeof(MapEntity) || ne.EntityId != _loadingMapEntityId)
{
return;
}
_loadingMapEntityId = 0;
_currentMap = null;
_state = SchedulerState.Failed;
Log.Error(
"Map load failed. LevelId={0}, Asset='{1}', Error='{2}'.",
_currentLevel != null ? _currentLevel.Id : 0,
ne.EntityAssetName,
ne.ErrorMessage);
}
private void OnHideEntityComplete(object sender, GameEventArgs e)
private void OnHideEntityComplete(HideEntityCompleteEventArgs args)
{
if (!(e is HideEntityCompleteEventArgs ne))
{
return;
}
_loadSession.HandleHideEntityComplete(args);
}
if (ne.EntityId == _loadedMapEntityId)
private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args)
{
var status = _loadSession.HandleOpenUIFormSuccess(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_loadedMapEntityId = 0;
_currentMap = null;
_state = SchedulerState.Failed;
Log.Error("CombatScheduler failed. {0}", errorMessage);
}
}
if (ne.EntityId == _loadingMapEntityId)
private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args)
{
var status = _loadSession.HandleOpenUIFormFailure(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_loadingMapEntityId = 0;
_state = SchedulerState.Failed;
Log.Error("CombatScheduler failed. {0}", errorMessage);
}
}
private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
{
_loadSession.HandleCloseUIFormComplete(args);
}
#endregion
}
}
@@ -0,0 +1,144 @@
using System.Collections.Generic;
using System.Globalization;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class PhaseLoopRuntime
{
private readonly List<DRLevelPhase> _phases = new();
private DRLevelPhase _currentPhase;
private int _currentPhaseIndex = -1;
private int _displayPhaseIndex;
private int _completedLoopCount;
private bool _canEndCombat;
private bool _endCombatRequested;
private float _currentPhaseElapsed;
public DRLevelPhase CurrentPhase => _currentPhase;
public int DisplayPhaseIndex => _displayPhaseIndex;
public bool CanEndCombat => _canEndCombat;
public float CurrentPhaseElapsed => _currentPhaseElapsed;
public int PhaseCount => _phases.Count;
public void Reset()
{
_phases.Clear();
_currentPhase = null;
_currentPhaseIndex = -1;
_displayPhaseIndex = 0;
_completedLoopCount = 0;
_canEndCombat = false;
_endCombatRequested = false;
_currentPhaseElapsed = 0f;
}
public void SetPhases(IReadOnlyList<DRLevelPhase> phases)
{
_phases.Clear();
if (phases == null)
{
return;
}
for (int i = 0; i < phases.Count; i++)
{
DRLevelPhase phase = phases[i];
if (phase != null)
{
_phases.Add(phase);
}
}
}
public bool TryRequestEndCombat()
{
if (!_canEndCombat)
{
return false;
}
_endCombatRequested = true;
return true;
}
public void AdvancePhaseElapsed(float elapseSeconds)
{
_currentPhaseElapsed += elapseSeconds;
}
public bool ShouldEndCurrentPhase(bool isPhaseSpawnCompleted, int aliveEnemyCount)
{
if (_currentPhase == null)
{
return false;
}
switch (_currentPhase.EndType)
{
case PhaseEndType.TimeElapsed:
return _currentPhaseElapsed >= ResolveTimeElapsedThreshold(_currentPhase);
case PhaseEndType.EnemiesCleared:
case PhaseEndType.BossDead:
return isPhaseSpawnCompleted && aliveEnemyCount <= 0;
case PhaseEndType.None:
default:
if (_currentPhase.DurationSeconds > 0 && _currentPhaseElapsed >= _currentPhase.DurationSeconds)
{
return true;
}
return isPhaseSpawnCompleted && aliveEnemyCount <= 0;
}
}
public bool TryEnterNextPhase(out DRLevelPhase nextPhase)
{
nextPhase = null;
if (_phases.Count <= 0)
{
return false;
}
_currentPhaseIndex++;
if (_currentPhaseIndex >= _phases.Count)
{
_completedLoopCount++;
_canEndCombat = true;
if (_endCombatRequested)
{
_currentPhase = null;
return false;
}
_currentPhaseIndex = 0;
}
_currentPhase = _phases[_currentPhaseIndex];
_displayPhaseIndex = _completedLoopCount * _phases.Count + _currentPhaseIndex + 1;
_currentPhaseElapsed = 0f;
nextPhase = _currentPhase;
return true;
}
private static float ResolveTimeElapsedThreshold(DRLevelPhase phase)
{
if (!string.IsNullOrWhiteSpace(phase.EndParam) &&
float.TryParse(phase.EndParam, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed))
{
return parsed;
}
if (phase.DurationSeconds > 0)
{
return phase.DurationSeconds;
}
return 0f;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2bdde0f128bc4f249a3e1254b8fd2d06
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: