This commit is contained in:
2026-04-30 19:33:16 +08:00
commit fb2252f688
556 changed files with 69721 additions and 0 deletions
@@ -0,0 +1,396 @@
using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
/// <summary>
/// 鎴樻枟鑺傜偣缁勪欢
/// </summary>
public class CombatNodeComponent : GameFrameworkComponent
{
// Level.Id => Level
private readonly Dictionary<int, DRLevel> _levelsById = new();
// LevelId => LevelPhases
private readonly Dictionary<int, List<DRLevelPhase>> _phasesByLevelId = new();
// LevelPhase.Id => LevelSpawnEntries
private readonly Dictionary<int, List<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _selectedSpawnEntriesByPhaseId = new();
private readonly List<int> _levelIdBuffer = new();
private readonly CombatScheduler _combatScheduler = new CombatScheduler();
private bool _runtimeInitialized;
private bool _isCombatActive;
private bool _lastCombatSucceeded = true;
public LevelThemeType CurrentThemeType { get; private set; }
public DRLevel CurrentLevel { get; private set; }
public int CurrentCoin => _isCombatActive ? _combatScheduler.CurrentCoin : 0;
public int CurrentGold => _isCombatActive ? _combatScheduler.CurrentGold : 0;
public int CurrentBaseHp => _isCombatActive ? _combatScheduler.CurrentBaseHp : 0;
public bool LastCombatSucceeded => _lastCombatSucceeded;
public bool CanEndCombat => _combatScheduler.CanEndCombat;
public int CurrentBuildTowerCount => _isCombatActive ? _combatScheduler.CurrentBuildTowerCount : 0;
public int LastDefeatedEnemyCount { get; private set; }
public int LastGainedCoin { get; private set; }
public int LastGainedGold { get; private set; }
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)
{
ShutdownCombatRuntime();
CurrentThemeType = themeType;
CurrentLevel = null;
_isCombatActive = false;
_lastCombatSucceeded = true;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
_levelsById.Clear();
_phasesByLevelId.Clear();
_spawnEntriesByPhaseId.Clear();
_selectedSpawnEntriesByPhaseId.Clear();
_levelIdBuffer.Clear();
IDataTable<DRLevel> dtLevel = GameEntry.DataTable.GetDataTable<DRLevel>();
IDataTable<DRLevelPhase> dtLevelPhase = GameEntry.DataTable.GetDataTable<DRLevelPhase>();
IDataTable<DRLevelSpawnEntry> dtSpawnEntry = GameEntry.DataTable.GetDataTable<DRLevelSpawnEntry>();
if (dtLevel == null || dtLevelPhase == null || dtSpawnEntry == null)
{
Log.Warning("CombatNodeComponent init failed. Missing data table(s).");
return;
}
DRLevel[] levels = dtLevel.GetAllDataRows();
for (int i = 0; i < levels.Length; i++)
{
DRLevel level = levels[i];
if (level.LevelThemeType != themeType)
{
continue;
}
_levelsById[level.Id] = level;
_phasesByLevelId[level.Id] = new List<DRLevelPhase>();
_levelIdBuffer.Add(level.Id);
}
DRLevelPhase[] levelPhases = dtLevelPhase.GetAllDataRows();
foreach (var phase in levelPhases)
{
int levelId = phase.Id / 1000;
if (!_levelsById.ContainsKey(levelId))
{
continue;
}
if (!_phasesByLevelId.TryGetValue(levelId, out List<DRLevelPhase> phases))
{
phases = new List<DRLevelPhase>();
_phasesByLevelId[levelId] = phases;
}
phases.Add(phase);
_spawnEntriesByPhaseId[phase.Id] = new List<DRLevelSpawnEntry>();
}
DRLevelSpawnEntry[] spawnEntries = dtSpawnEntry.GetAllDataRows();
foreach (var spawnEntry in spawnEntries)
{
int phaseId = spawnEntry.Id / 1000;
int levelId = phaseId / 1000;
if (!_levelsById.ContainsKey(levelId))
{
continue;
}
if (!_spawnEntriesByPhaseId.TryGetValue(phaseId, out List<DRLevelSpawnEntry> entries))
{
entries = new List<DRLevelSpawnEntry>();
_spawnEntriesByPhaseId[phaseId] = entries;
}
entries.Add(spawnEntry);
}
foreach (List<DRLevelPhase> phaseList in _phasesByLevelId.Values)
{
phaseList.Sort((left, right) => left.Id.CompareTo(right.Id));
}
foreach (List<DRLevelSpawnEntry> phaseSpawnEntries in _spawnEntriesByPhaseId.Values)
{
phaseSpawnEntries.Sort((left, right) =>
{
int timeCompare = left.StartTime.CompareTo(right.StartTime);
if (timeCompare != 0)
{
return timeCompare;
}
return left.Id.CompareTo(right.Id);
});
}
Log.Info(
"CombatNodeComponent initialized. Theme={0}, Levels={1}, Phases={2}, Entries={3}.",
themeType,
_levelsById.Count,
CountPhases(),
CountEntries());
EnsureCombatRuntimeInitialized();
}
public void StartCombat(
int levelId = 0,
RunNodeExecutionContext context = null)
{
if (!EnsureCombatRuntimeInitialized())
{
Log.Warning("CombatNodeComponent start failed. Missing scheduler runtime.");
return;
}
bool hasLevel = levelId > 0
? TryGetLevelById(levelId, out DRLevel selectedLevel)
: TrySelectRandomLevel(out selectedLevel);
if (!hasLevel)
{
Log.Warning(
"CombatNodeComponent start failed. LevelId={0}. Missing cached level data or invalid level id.",
levelId);
return;
}
if (!_phasesByLevelId.TryGetValue(selectedLevel.Id, out List<DRLevelPhase> phaseList) ||
phaseList == null ||
phaseList.Count <= 0)
{
Log.Warning("CombatNodeComponent start failed. Level '{0}' has no phase data.", selectedLevel.Id);
return;
}
_selectedSpawnEntriesByPhaseId.Clear();
foreach (var phase in phaseList)
{
if (_spawnEntriesByPhaseId.TryGetValue(phase.Id, out List<DRLevelSpawnEntry> entries) &&
entries != null)
{
_selectedSpawnEntriesByPhaseId[phase.Id] = entries;
}
else
{
_selectedSpawnEntriesByPhaseId[phase.Id] = new List<DRLevelSpawnEntry>();
}
}
CurrentLevel = selectedLevel;
_isCombatActive = false;
_lastCombatSucceeded = true;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
if (!_combatScheduler.Start(
selectedLevel,
phaseList,
_selectedSpawnEntriesByPhaseId,
context))
{
CurrentLevel = null;
return;
}
_isCombatActive = true;
}
public void EndCombat()
{
if (!_isCombatActive)
{
return;
}
_isCombatActive = false;
LastDefeatedEnemyCount = _combatScheduler.DefeatedEnemyCount;
LastGainedCoin = _combatScheduler.GainedCoin;
LastGainedGold = _combatScheduler.GainedGold;
CurrentLevel = null;
}
public bool TryEndCombatByPlayer()
{
return _combatScheduler.TryEndCombatByPlayer();
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_runtimeInitialized)
{
return;
}
_combatScheduler.OnUpdate(elapseSeconds, realElapseSeconds);
}
public void OnShutdown()
{
_isCombatActive = false;
CurrentLevel = null;
_lastCombatSucceeded = true;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
ShutdownCombatRuntime();
}
public void OnCombatEndedByScheduler(bool succeeded)
{
_lastCombatSucceeded = succeeded;
EndCombat();
}
public bool TryConsumeCoin(int coin)
{
if (Mathf.Max(0, coin) <= 0)
{
return true;
}
if (!_isCombatActive)
{
return false;
}
return _combatScheduler.TryConsumeCoin(coin);
}
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
{
if (!_isCombatActive)
{
stats = null;
return false;
}
return _combatScheduler.TryGetBuildTowerStats(buildIndex, out stats);
}
public void AddCoin(int coin)
{
if (!_isCombatActive)
{
return;
}
_combatScheduler.AddCoin(coin);
}
private void OnDestroy()
{
OnShutdown();
}
private bool EnsureCombatRuntimeInitialized()
{
if (_runtimeInitialized)
{
return true;
}
_combatScheduler.OnInit(OnCombatEndedByScheduler);
_runtimeInitialized = true;
return true;
}
private void ShutdownCombatRuntime()
{
if (!_runtimeInitialized)
{
return;
}
_combatScheduler.OnDestroy();
_runtimeInitialized = false;
}
private bool TrySelectRandomLevel(out DRLevel level)
{
level = null;
if (_levelIdBuffer.Count <= 0)
{
return false;
}
int selectedIndex = Random.Range(0, _levelIdBuffer.Count);
int selectedLevelId = _levelIdBuffer[selectedIndex];
return _levelsById.TryGetValue(selectedLevelId, out level);
}
private bool TryGetLevelById(int levelId, out DRLevel level)
{
level = null;
if (levelId <= 0)
{
return false;
}
return _levelsById.TryGetValue(levelId, out level);
}
private int CountPhases()
{
int count = 0;
foreach (List<DRLevelPhase> phases in _phasesByLevelId.Values)
{
count += phases.Count;
}
return count;
}
private int CountEntries()
{
int count = 0;
foreach (List<DRLevelSpawnEntry> list in _spawnEntriesByPhaseId.Values)
{
count += list.Count;
}
return count;
}
}
}
@@ -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,315 @@
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 _combatInfoFormUseCase;
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, MapEntityLoadContext mapLoadContext, ICombatSchedulerPort schedulerPort, out string errorMessage)
{
errorMessage = null;
if (_entity == null)
{
errorMessage = "CombatLoadSession start failed. Entity component is null.";
return false;
}
if (!TryShowMap(level, mapLoadContext, out errorMessage))
{
return false;
}
if (!TryOpenCombatInfoForm(schedulerPort, 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, MapEntityLoadContext mapLoadContext, out string errorMessage)
{
errorMessage = null;
if (level == null)
{
errorMessage = "CombatLoadSession map load failed. Level is null.";
return false;
}
if (mapLoadContext?.InitialMapData == null)
{
errorMessage = "CombatLoadSession map load failed. MapEntityLoadContext is missing initial map data.";
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();
MapData resolvedMapData = mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero);
MapEntityLoadContext resolvedLoadContext = new MapEntityLoadContext(
resolvedMapData,
mapLoadContext.TryConsumeCoin,
mapLoadContext.AddCoin);
_entity.ShowMap(resolvedLoadContext, mapAssetName);
return true;
}
private bool TryOpenCombatInfoForm(ICombatSchedulerPort schedulerPort, out string errorMessage)
{
errorMessage = null;
if (_combatInfoFormUseCase == null)
{
_combatInfoFormUseCase = new CombatInfoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _combatInfoFormUseCase);
}
_combatInfoFormUseCase.Configure(
() => BuildCombatInfoFormRawData(schedulerPort),
() => schedulerPort != null && schedulerPort.CanEndCombat && schedulerPort.TryEndCombatByPlayer(),
() => schedulerPort != null && schedulerPort.TryDebugFail("Manual debug fail from CombatInfoForm."));
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 static CombatInfoFormRawData BuildCombatInfoFormRawData(ICombatSchedulerPort schedulerPort)
{
if (schedulerPort == null)
{
return null;
}
DRLevel level = schedulerPort.CurrentLevel;
LevelThemeType themeType = level != null ? level.LevelThemeType : LevelThemeType.None;
int levelId = level != null ? level.Id : 0;
int baseHpMax = level != null ? Mathf.Max(0, level.BaseHp) : 0;
return CombatInfoFormUseCase.BuildRawData(
themeType,
levelId,
schedulerPort.DisplayPhaseIndex,
schedulerPort.PhaseCount,
schedulerPort.CurrentCoin,
schedulerPort.CurrentBaseHp,
baseHpMax,
schedulerPort.CanEndCombat);
}
private void CloseCombatInfoForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.CombatInfoForm);
_loadingCombatInfoFormSerialId = null;
_isCombatInfoFormReady = false;
}
}
}
@@ -0,0 +1,288 @@
using System.Collections.Generic;
using GeometryTD.CustomEvent;
using GeometryTD.CustomUtility;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
public sealed class CombatRunResourceStore
{
private readonly List<TowerStatsData> _buildTowerStatsSnapshot = new();
private readonly List<TowerItemData> _participantTowerSnapshot = new();
private readonly BackpackInventoryData _rewardInventory = new();
private BackpackInventoryData _combatInventorySnapshot = new();
private bool _isCombatActive;
public int CurrentCoin { get; private set; }
public int CurrentGold => _isCombatActive ? GainedGold : 0;
public int CurrentBaseHp { get; private set; }
public int MaxBaseHp { get; private set; }
public int CurrentBuildTowerCount => _isCombatActive ? _buildTowerStatsSnapshot.Count : 0;
public int GainedCoin { get; private set; }
public int GainedGold { get; private set; }
public void InitializeForCombat(DRLevel level)
{
Reset();
if (level == null)
{
return;
}
MaxBaseHp = Mathf.Max(0, level.BaseHp);
CurrentBaseHp = MaxBaseHp;
CurrentCoin = Mathf.Max(0, level.StartCoin);
CacheCombatSnapshots();
_isCombatActive = true;
}
public void MarkCombatEnded()
{
_isCombatActive = false;
}
public void Reset()
{
_isCombatActive = false;
CurrentCoin = 0;
CurrentBaseHp = 0;
MaxBaseHp = 0;
GainedCoin = 0;
GainedGold = 0;
_buildTowerStatsSnapshot.Clear();
_participantTowerSnapshot.Clear();
_combatInventorySnapshot = new BackpackInventoryData();
_rewardInventory.Gold = 0;
_rewardInventory.MuzzleComponents.Clear();
_rewardInventory.BearingComponents.Clear();
_rewardInventory.BaseComponents.Clear();
_rewardInventory.Towers.Clear();
_rewardInventory.ParticipantTowerInstanceIds.Clear();
}
public BackpackInventoryData GetRewardInventorySnapshot()
{
return InventoryCloneUtility.CloneInventory(_rewardInventory);
}
public BackpackInventoryData GetCombatInventorySnapshot()
{
return InventoryCloneUtility.CloneInventory(_combatInventorySnapshot);
}
public IReadOnlyList<TowerItemData> GetParticipantTowerSnapshot()
{
List<TowerItemData> snapshot = new List<TowerItemData>(_participantTowerSnapshot.Count);
for (int i = 0; i < _participantTowerSnapshot.Count; i++)
{
TowerItemData tower = _participantTowerSnapshot[i];
if (tower != null)
{
snapshot.Add(InventoryCloneUtility.CloneTower(tower));
}
}
return snapshot;
}
public IReadOnlyList<long> GetParticipantTowerInstanceIdSnapshot()
{
List<long> snapshot = new List<long>(_participantTowerSnapshot.Count);
for (int i = 0; i < _participantTowerSnapshot.Count; i++)
{
TowerItemData tower = _participantTowerSnapshot[i];
if (tower != null && tower.InstanceId > 0)
{
snapshot.Add(tower.InstanceId);
}
}
return snapshot;
}
public bool TryConsumeCoin(int coin)
{
int requiredCoin = Mathf.Max(0, coin);
if (requiredCoin <= 0)
{
return true;
}
if (!_isCombatActive || CurrentCoin < requiredCoin)
{
return false;
}
CurrentCoin -= requiredCoin;
FireCoinChangedEvent(-requiredCoin);
return true;
}
public void AddCoin(int coin)
{
int gainedCoin = Mathf.Max(0, coin);
if (!_isCombatActive || gainedCoin <= 0)
{
return;
}
CurrentCoin += gainedCoin;
FireCoinChangedEvent(gainedCoin);
}
public int ApplyBaseDamage(int damage)
{
int resolvedDamage = Mathf.Max(0, damage);
if (!_isCombatActive || resolvedDamage <= 0)
{
return CurrentBaseHp;
}
int previousBaseHp = CurrentBaseHp;
CurrentBaseHp = Mathf.Max(0, CurrentBaseHp - resolvedDamage);
int deltaBaseHp = CurrentBaseHp - previousBaseHp;
if (deltaBaseHp != 0)
{
FireBaseHpChangedEvent(deltaBaseHp);
}
return CurrentBaseHp;
}
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
{
stats = null;
if (!_isCombatActive || buildIndex < 0 || buildIndex >= _buildTowerStatsSnapshot.Count)
{
return false;
}
TowerStatsData sourceStats = _buildTowerStatsSnapshot[buildIndex];
if (sourceStats == null)
{
return false;
}
stats = InventoryCloneUtility.CloneTowerStats(sourceStats);
return stats != null;
}
public void AddEnemyDefeatedReward(int gainedCoin, int gainedGold)
{
if (!_isCombatActive)
{
return;
}
int coin = Mathf.Max(0, gainedCoin);
int gold = Mathf.Max(0, gainedGold);
GainedCoin += coin;
GainedGold += gold;
if (coin > 0)
{
CurrentCoin += coin;
FireCoinChangedEvent(coin);
}
if (gold > 0)
{
_rewardInventory.Gold += gold;
}
}
public void AddSettlementGold(int gainedGold)
{
int gold = Mathf.Max(0, gainedGold);
if (gold <= 0)
{
return;
}
GainedGold += gold;
_rewardInventory.Gold += gold;
}
public void AddEnemyDefeatedLoot(TowerCompItemData droppedItem)
{
if (!_isCombatActive || droppedItem == null)
{
return;
}
AppendRewardItem(droppedItem);
}
private void CacheCombatSnapshots()
{
_buildTowerStatsSnapshot.Clear();
_participantTowerSnapshot.Clear();
_combatInventorySnapshot = GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: new BackpackInventoryData();
if (GameEntry.PlayerInventory == null)
{
return;
}
IReadOnlyList<TowerItemData> towers = GameEntry.PlayerInventory.GetParticipantTowerSnapshot();
if (towers == null || towers.Count <= 0)
{
return;
}
for (int i = 0; i < towers.Count; i++)
{
TowerItemData tower = towers[i];
if (tower == null)
{
continue;
}
_participantTowerSnapshot.Add(InventoryCloneUtility.CloneTower(tower));
if (tower.Stats != null)
{
_buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats));
}
}
}
private void FireCoinChangedEvent(int deltaCoin)
{
if (!_isCombatActive)
{
return;
}
GameEntry.Event.Fire(this, CombatCoinChangedEventArgs.Create(CurrentCoin, deltaCoin));
}
private void FireBaseHpChangedEvent(int deltaBaseHp)
{
if (!_isCombatActive)
{
return;
}
GameEntry.Event.Fire(this, CombatBaseHpChangedEventArgs.Create(CurrentBaseHp, deltaBaseHp));
}
private void AppendRewardItem(TowerCompItemData droppedItem)
{
switch (droppedItem)
{
case MuzzleCompItemData muzzleComp:
_rewardInventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
break;
case BearingCompItemData bearingComp:
_rewardInventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
break;
case BaseCompItemData baseComp:
_rewardInventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
break;
}
}
}
}
@@ -0,0 +1,327 @@
using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.Procedure;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler : ICombatSchedulerPort
{
private readonly CombatSchedulerRuntime _runtime = new();
private readonly CombatSchedulerCoordinator _coordinator;
private bool _initialized;
public CombatScheduler()
{
_coordinator = new CombatSchedulerCoordinator(this, _runtime);
}
public bool IsRunning =>
_runtime.CurrentState is CombatLoadingState or CombatRunningPhaseState or CombatWaitingForPhaseEndState;
public bool IsCompleted => _runtime.IsCompleted;
public DRLevel CurrentLevel => _runtime.CurrentLevel;
public DRLevelPhase CurrentPhase => _runtime.PhaseLoopRuntime.CurrentPhase;
public MapEntity CurrentMap => _runtime.LoadSession.CurrentMap;
public int DisplayPhaseIndex => _runtime.PhaseLoopRuntime.DisplayPhaseIndex;
public int PhaseCount => _runtime.PhaseLoopRuntime.PhaseCount;
public bool CanEndCombat => _runtime.PhaseLoopRuntime.CanEndCombat;
public int CurrentCoin => _runtime.CombatRunResourceStore.CurrentCoin;
public int CurrentGold => _runtime.CombatRunResourceStore.CurrentGold;
public int CurrentBaseHp => _runtime.CombatRunResourceStore.CurrentBaseHp;
public int CurrentBuildTowerCount => _runtime.CombatRunResourceStore.CurrentBuildTowerCount;
public int DefeatedEnemyCount => _runtime.EnemyManager.DefeatedEnemyCount;
public int GainedCoin => _runtime.CombatRunResourceStore.GainedCoin;
public int GainedGold => _runtime.CombatRunResourceStore.GainedGold;
public void OnInit(Action<bool> combatEndedCallback)
{
_runtime.CombatEndedCallback = combatEndedCallback;
if (!_initialized)
{
_runtime.Entity = GameEntry.Entity;
_runtime.EventBridge.Bind(
OnShowEntitySuccess,
OnShowEntityFailure,
OnHideEntityComplete,
OnOpenUIFormSuccess,
OnOpenUIFormFailure,
OnCloseUIFormComplete);
_runtime.EnemyManager.OnInit(this);
_runtime.LoadSession.OnInit(_runtime.Entity);
_coordinator.EnsureCombatFinishFormUseCaseBound();
_coordinator.EnsureRewardSelectFormUseCaseBound();
_initialized = true;
}
_coordinator.ResetRuntime();
}
public bool Start(
DRLevel level,
IReadOnlyList<DRLevelPhase> phases,
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId,
RunNodeExecutionContext context = null)
{
if (!_initialized || _runtime.Entity == null)
{
return _coordinator.HandleStartFailure("CombatScheduler start failed. Runtime is not initialized.");
}
if (level == null || phases == null || phases.Count <= 0)
{
return _coordinator.HandleStartFailure("CombatScheduler start failed. Invalid level or phase data.");
}
_coordinator.CleanupAllCombatEntities();
_coordinator.CloseCombatFinishForm();
_coordinator.CloseRewardSelectForm();
_coordinator.CloseDialogForm();
_runtime.EnemyManager.EndPhase();
_runtime.EnemyManager.ResetCombatStats();
_coordinator.ResetRuntime();
_runtime.DidCombatWin = true;
_runtime.CurrentLevel = level;
_runtime.NodeContext = context != null ? context.Clone() : null;
_runtime.NextDropOrdinal = 0;
_runtime.NextRewardOrdinal = 0;
_runtime.CombatRunResourceStore.InitializeForCombat(level);
foreach (var phase in phases)
{
if (phase != null)
{
_runtime.PhaseBuffer.Add(phase);
}
}
if (spawnEntriesByPhaseId != null)
{
foreach (var pair in spawnEntriesByPhaseId)
{
_runtime.SpawnEntriesByPhaseId[pair.Key] = pair.Value;
}
}
_runtime.PhaseLoopRuntime.SetPhases(_runtime.PhaseBuffer);
if (_runtime.PhaseLoopRuntime.PhaseCount <= 0)
{
return _coordinator.HandleStartFailure(
$"CombatScheduler start failed. Level '{level.Id}' has no phase data.");
}
ChangeState(new CombatLoadingState(_runtime, _coordinator));
Log.Info(
"CombatScheduler started. Level={0}, PhaseCount={1}.",
_runtime.CurrentLevel.Id,
_runtime.PhaseLoopRuntime.PhaseCount);
return true;
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_runtime.CurrentState?.OnUpdate(elapseSeconds, realElapseSeconds);
}
public void OnDestroy()
{
if (!_initialized)
{
return;
}
_runtime.CurrentState?.OnExit();
_runtime.CurrentState?.OnDestroy();
_runtime.CurrentState = null;
_coordinator.CleanupAllCombatEntities();
_coordinator.CloseCombatFinishForm();
_coordinator.CloseRewardSelectForm();
_coordinator.CloseDialogForm();
_runtime.EnemyManager.OnDestroy();
_coordinator.ResetRuntime();
_runtime.EventBridge.Unbind();
_runtime.CombatFinishFormUseCase = null;
_runtime.RewardSelectFormUseCase = null;
_runtime.CombatEndedCallback = null;
_runtime.Entity = null;
_initialized = false;
}
public bool TryEndCombatByPlayer()
{
if (_runtime.CurrentState is not CombatRunningPhaseState &&
_runtime.CurrentState is not CombatWaitingForPhaseEndState)
{
return false;
}
return _runtime.PhaseLoopRuntime.TryRequestEndCombat();
}
public void OnEnemyReachedBase(DREnemy enemy)
{
if (!IsRunning)
{
return;
}
int resolvedBaseDamage = enemy != null ? Mathf.Max(0, enemy.BaseDamage) : 0;
if (resolvedBaseDamage <= 0)
{
return;
}
_coordinator.ApplyBaseDamage(resolvedBaseDamage);
}
public void OnEnemyDefeated(DREnemy enemy)
{
if (!IsRunning)
{
return;
}
EnemyDropContext context = new(
enemy,
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
_coordinator.ResolveCurrentThemeType());
int nextDropOrdinal = _runtime.NextDropOrdinal;
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(
context,
_runtime.NodeContext != null ? _runtime.NodeContext.RunSeed : 0,
_runtime.NodeContext != null ? _runtime.NodeContext.SequenceIndex : -1,
ref nextDropOrdinal);
_runtime.NextDropOrdinal = nextDropOrdinal;
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
}
public bool OnCombatFinishReturnRequested()
{
if (_runtime.CurrentState is not CombatWaitingForReturnState waitingForReturnState)
{
return false;
}
waitingForReturnState.RequestReturn();
return true;
}
public bool TryConsumeCoin(int coin)
{
return _runtime.CombatRunResourceStore.TryConsumeCoin(coin);
}
public void AddCoin(int coin)
{
_runtime.CombatRunResourceStore.AddCoin(coin);
}
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
{
return _runtime.CombatRunResourceStore.TryGetBuildTowerStats(buildIndex, out stats);
}
public bool TryDebugFail(string errorMessage)
{
if (_runtime.IsCompleted || _runtime.CurrentState == null || _runtime.CurrentState is CombatFailedState)
{
return false;
}
_coordinator.EnterFailureFallback(string.IsNullOrWhiteSpace(errorMessage)
? "Manual debug fail."
: errorMessage);
return _runtime.CurrentState is CombatFailedState;
}
void ICombatSchedulerPort.ChangeState(CombatStateBase nextState)
{
ChangeState(nextState);
}
internal void ChangeState(CombatStateBase nextState)
{
if (ReferenceEquals(_runtime.CurrentState, nextState))
{
return;
}
_runtime.CurrentState?.OnExit();
_runtime.CurrentState?.OnDestroy();
_runtime.CurrentState = nextState;
_runtime.CurrentState?.OnInit();
_runtime.CurrentState?.OnEnter();
}
#region Event Handlers
private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args)
{
var status = _runtime.LoadSession.HandleShowEntitySuccess(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_coordinator.EnterFailureFallback(errorMessage);
return;
}
if (status == CombatLoadSession.EventHandleStatus.Succeeded)
{
MapEntity map = _runtime.LoadSession.CurrentMap;
Log.Info(
"Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.",
_runtime.CurrentLevel != null ? _runtime.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 = _runtime.LoadSession.HandleShowEntityFailure(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_coordinator.EnterFailureFallback(errorMessage);
}
}
private void OnHideEntityComplete(HideEntityCompleteEventArgs args)
{
_runtime.LoadSession.HandleHideEntityComplete(args);
}
private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args)
{
var status = _runtime.LoadSession.HandleOpenUIFormSuccess(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_coordinator.EnterFailureFallback(errorMessage);
}
}
private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args)
{
var status = _runtime.LoadSession.HandleOpenUIFormFailure(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_coordinator.EnterFailureFallback(errorMessage);
}
}
private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
{
_runtime.LoadSession.HandleCloseUIFormComplete(args);
}
#endregion
}
}
@@ -0,0 +1,282 @@
using System.Collections.Generic;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatSchedulerCoordinator
{
private readonly ICombatSchedulerPort _schedulerPort;
private readonly CombatSchedulerRuntime _runtime;
public ICombatSchedulerPort Port => _schedulerPort;
public CombatSchedulerCoordinator(ICombatSchedulerPort schedulerPort, CombatSchedulerRuntime runtime)
{
_schedulerPort = schedulerPort;
_runtime = runtime;
}
public void ChangeState(CombatStateBase nextState)
{
_schedulerPort.ChangeState(nextState);
}
public 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;
}
public void ResetRuntime()
{
_runtime.CurrentState = null;
_runtime.PhaseBuffer.Clear();
_runtime.SpawnEntriesByPhaseId.Clear();
_runtime.PhaseLoopRuntime.Reset();
_runtime.LoadSession.Reset();
_runtime.CombatRunResourceStore.Reset();
_runtime.SettlementContext = null;
_runtime.CurrentLevel = null;
_runtime.DidCombatWin = true;
_runtime.IsCompleted = false;
_runtime.NodeEnterFired = false;
_runtime.NodeContext = null;
_runtime.NextDropOrdinal = 0;
_runtime.NextRewardOrdinal = 0;
}
public void CleanupAllCombatEntities()
{
_runtime.LoadSession.Cleanup();
_runtime.EnemyManager.CleanupTrackedEnemies();
}
public void EnsureCombatFinishFormUseCaseBound()
{
_runtime.CombatFinishFormUseCase ??= new CombatFinishFormUseCase(_schedulerPort);
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _runtime.CombatFinishFormUseCase);
}
public void EnsureRewardSelectFormUseCaseBound()
{
_runtime.RewardSelectFormUseCase ??= new RewardSelectFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _runtime.RewardSelectFormUseCase);
}
public void OpenCombatFailureDialog(string errorMessage)
{
CloseDialogForm();
GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData
{
Mode = 1,
Title = "Combat Error",
Message = string.IsNullOrWhiteSpace(errorMessage) ? "Combat failed unexpectedly." : errorMessage,
PauseGame = false,
ConfirmText = "Return Menu",
OnClickConfirm = OnCombatFailureDialogConfirmed
});
}
public bool TryBeginNextPhase()
{
if (!_runtime.PhaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
{
_schedulerPort.ChangeState(new CombatSettlementState(_runtime, this, true));
return false;
}
_runtime.SpawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries);
_schedulerPort.ChangeState(new CombatRunningPhaseState(_runtime, this, nextPhase, spawnEntries));
return true;
}
public void EnterWaitingForPhaseEnd()
{
_schedulerPort.ChangeState(new CombatWaitingForPhaseEndState(_runtime, this));
}
public void CompleteCurrentPhase()
{
_runtime.EnemyManager.EndPhase();
Log.Info(
"CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.",
_runtime.CurrentLevel != null ? _runtime.CurrentLevel.Id : 0,
_runtime.PhaseLoopRuntime.CurrentPhase != null ? _runtime.PhaseLoopRuntime.CurrentPhase.Id : 0,
_runtime.PhaseLoopRuntime.CurrentPhaseElapsed);
TryBeginNextPhase();
}
public bool ShouldEnterSettlementFromActiveState(out bool didCombatWin)
{
if (GetCurrentBaseHp() <= 0)
{
didCombatWin = false;
return true;
}
if (_runtime.PhaseLoopRuntime.IsEndCombatRequested)
{
didCombatWin = true;
return true;
}
didCombatWin = true;
return false;
}
public void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward)
{
if (_runtime.CurrentState is not CombatRewardSelectionState || _runtime.SettlementContext == null)
{
return;
}
_runtime.CombatSettlementService.ApplySelectedReward(_runtime.SettlementContext, selectedReward);
_schedulerPort.ChangeState(new CombatFinishFormState(_runtime, this));
}
public void OnFullBaseHpRewardGiveUp()
{
if (_runtime.CurrentState is not CombatRewardSelectionState || _runtime.SettlementContext == null)
{
return;
}
_schedulerPort.ChangeState(new CombatFinishFormState(_runtime, this));
}
public LevelThemeType ResolveCurrentThemeType()
{
if (_runtime.CurrentLevel != null)
{
return _runtime.CurrentLevel.LevelThemeType;
}
return LevelThemeType.None;
}
public int ApplyBaseDamage(int damage)
{
return _runtime.CombatRunResourceStore.ApplyBaseDamage(damage);
}
public int GetCurrentBaseHp()
{
return Mathf.Max(0, _runtime.CombatRunResourceStore.CurrentBaseHp);
}
public void CloseCombatFinishForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
}
public void CloseRewardSelectForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm);
}
public void CloseDialogForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
}
public void CompleteNormalCombatAndNotify(bool didCombatWin)
{
CompleteCombat(didCombatWin);
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
GameEntry.Event.Fire(
this,
NodeCompleteEventArgs.Create(
nodeContext?.RunId,
nodeContext?.NodeId ?? 0,
nodeContext?.NodeType ?? RunNodeType.None,
nodeContext?.SequenceIndex ?? -1,
RunNodeCompletionStatus.Completed,
didCombatWin,
nodeContext != null
? nodeContext.CreateCompletionSnapshot(
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
: null));
}
public void CompleteFailureCombatAndNotify()
{
CleanupAllCombatEntities();
CloseCombatFinishForm();
CloseRewardSelectForm();
CloseDialogForm();
CompleteCombat(false);
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
GameEntry.Event.Fire(
this,
NodeCompleteEventArgs.Create(
nodeContext?.RunId,
nodeContext?.NodeId ?? 0,
nodeContext?.NodeType ?? RunNodeType.None,
nodeContext?.SequenceIndex ?? -1,
RunNodeCompletionStatus.Exception,
false,
nodeContext != null
? nodeContext.CreateCompletionSnapshot(
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
: null));
}
public bool HandleStartFailure(string errorMessage)
{
Log.Warning("{0}", errorMessage);
_runtime.EnemyManager.EndPhase();
CleanupAllCombatEntities();
CloseCombatFinishForm();
CloseRewardSelectForm();
CloseDialogForm();
ResetRuntime();
return false;
}
public void EnterFailureFallback(string errorMessage)
{
if (_runtime.CurrentState is CombatFailedState || _runtime.IsCompleted)
{
return;
}
_schedulerPort.ChangeState(new CombatFailedState(_runtime, this, errorMessage));
}
public void OnCombatFailureDialogConfirmed(object userData)
{
_ = userData;
if (_runtime.CurrentState is not CombatFailedState || _runtime.IsCompleted)
{
return;
}
CompleteFailureCombatAndNotify();
}
private void CompleteCombat(bool succeeded)
{
_runtime.IsCompleted = true;
_runtime.CurrentState = null;
_runtime.CombatRunResourceStore.MarkCombatEnded();
_runtime.CombatEndedCallback?.Invoke(succeeded);
}
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Entity;
using GeometryTD.Procedure;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatSchedulerRuntime
{
public List<DRLevelPhase> PhaseBuffer { get; } = new();
public Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> SpawnEntriesByPhaseId { get; } = new();
public EnemyManager EnemyManager { get; } = new();
public PhaseLoopRuntime PhaseLoopRuntime { get; } = new();
public CombatLoadSession LoadSession { get; } = new();
public CombatEventBridge EventBridge { get; } = new();
public CombatRunResourceStore CombatRunResourceStore { get; } = new();
public CombatSettlementService CombatSettlementService { get; } = new();
public EntityComponent Entity { get; set; }
public DRLevel CurrentLevel { get; set; }
public CombatFinishFormUseCase CombatFinishFormUseCase { get; set; }
public RewardSelectFormUseCase RewardSelectFormUseCase { get; set; }
public CombatStateBase CurrentState { get; set; }
public Action<bool> CombatEndedCallback { get; set; }
public bool DidCombatWin { get; set; } = true;
public bool IsCompleted { get; set; }
public bool NodeEnterFired { get; set; }
public CombatSettlementContext SettlementContext { get; set; }
public RunNodeExecutionContext NodeContext { get; set; }
public int NextDropOrdinal { get; set; }
public int NextRewardOrdinal { get; set; }
}
}
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
public sealed class CombatSettlementContext
{
public CombatSettlementFlags Flags { get; } = new();
public CombatSettlementResult Result { get; } = new();
public CombatSettlementSummary Summary { get; } = new();
}
public sealed class CombatSettlementFlags
{
public bool ShouldOpenRewardSelection;
public bool DidEnterRewardSelection;
public bool IsCommitted;
}
public sealed class CombatSettlementResult
{
public bool DidCombatWin;
public int FinalCoin;
public int FinalBaseHp;
public int MaxBaseHp;
public int DefeatedEnemyCount;
public int GainedGold;
public BackpackInventoryData RewardInventory;
public CombatSettlementEnduranceResult Endurance { get; } = new();
}
public sealed class CombatSettlementEnduranceResult
{
public List<long> TargetTowerInstanceIds { get; } = new();
public float EnduranceLossPerComponent;
public int AffectedTowerCount;
}
public sealed class CombatSettlementSummary
{
public int DefeatedEnemyCount;
public int GainedGold;
public BackpackInventoryData RewardInventory;
}
}
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public sealed class CombatSettlementService
{
private const int RewardSelectDisplayCount = 3;
private readonly CombatSettlementCalculator _calculator = new();
private readonly CombatSettlementCommitter _committer = new();
public CombatSettlementContext BuildSettlementContext(
bool didCombatWin,
DRLevel currentLevel,
int defeatedEnemyCount,
CombatRunResourceStore resourceStore)
{
return _calculator.BuildSettlementContext(
didCombatWin,
currentLevel,
defeatedEnemyCount,
resourceStore);
}
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
{
_committer.CommitSettlementInventory(settlementContext);
}
public bool TryPrepareRewardSelection(
CombatSettlementContext settlementContext,
int displayPhaseIndex,
LevelThemeType themeType,
int runSeed,
int sequenceIndex,
ref int nextRewardOrdinal,
RewardSelectFormUseCase rewardSelectFormUseCase,
Action<RewardSelectItemRawData> onRewardSelected,
Action onGiveUp)
{
if (settlementContext == null || rewardSelectFormUseCase == null)
{
return false;
}
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
displayPhaseIndex,
themeType,
RewardSelectDisplayCount,
runSeed,
sequenceIndex,
ref nextRewardOrdinal);
if (candidateItems == null || candidateItems.Count <= 0)
{
settlementContext.Flags.ShouldOpenRewardSelection = false;
return false;
}
rewardSelectFormUseCase.SetCallbacks(onRewardSelected, onGiveUp);
rewardSelectFormUseCase.ConfigureRewardCandidates(
candidateItems,
displayCount: RewardSelectDisplayCount,
refreshCost: 0,
allowRotateOnce: false,
allowGiveUp: false,
tipText: "基地满血奖励:请选择 1 个组件");
RewardSelectFormRawData rawData = rewardSelectFormUseCase.CreateInitialModel();
if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0)
{
settlementContext.Flags.ShouldOpenRewardSelection = false;
return false;
}
settlementContext.Flags.DidEnterRewardSelection = true;
GameEntry.UIRouter.OpenUI(UIFormType.RewardSelectForm, rawData);
return true;
}
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
{
_committer.ApplySelectedReward(settlementContext, selectedReward);
}
public void OpenCombatFinishForm(
CombatSettlementContext settlementContext,
CombatFinishFormUseCase combatFinishFormUseCase)
{
if (settlementContext == null || combatFinishFormUseCase == null)
{
return;
}
combatFinishFormUseCase.SetSummary(settlementContext);
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
}
}
}
@@ -0,0 +1,34 @@
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatFailedState : CombatStateBase
{
private readonly string _errorMessage;
public CombatFailedState(
CombatSchedulerRuntime runtime,
CombatSchedulerCoordinator coordinator,
string errorMessage) : base(runtime, coordinator)
{
_errorMessage = errorMessage;
}
public override void OnEnter()
{
Log.Error(
"CombatScheduler failed. LevelId={0}, {1}",
Runtime.CurrentLevel != null ? Runtime.CurrentLevel.Id : 0,
_errorMessage);
Runtime.EnemyManager.EndPhase();
Coordinator.CloseCombatFinishForm();
Coordinator.CloseRewardSelectForm();
Coordinator.OpenCombatFailureDialog(_errorMessage);
}
public override void OnExit()
{
Coordinator.CloseDialogForm();
}
}
}
@@ -0,0 +1,25 @@
namespace GeometryTD.CustomComponent
{
internal sealed class CombatFinishFormState : CombatStateBase
{
public CombatFinishFormState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
: base(runtime, coordinator)
{
}
public override void OnEnter()
{
if (Runtime.SettlementContext == null)
{
Coordinator.EnterFailureFallback("Combat finish form failed. Settlement context is missing.");
return;
}
Coordinator.EnsureCombatFinishFormUseCaseBound();
Runtime.CombatSettlementService.OpenCombatFinishForm(
Runtime.SettlementContext,
Runtime.CombatFinishFormUseCase);
Coordinator.ChangeState(new CombatWaitingForReturnState(Runtime, Coordinator));
}
}
}
@@ -0,0 +1,71 @@
using System.Collections.Generic;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
using GeometryTD.UI;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatLoadingState : CombatStateBase
{
public CombatLoadingState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
: base(runtime, coordinator)
{
}
public override void OnEnter()
{
if (Runtime.CurrentLevel == null)
{
Coordinator.EnterFailureFallback("Combat loading failed. Current level is null.");
return;
}
MapEntityLoadContext mapLoadContext = BuildMapLoadContext();
if (!Runtime.LoadSession.StartLoading(Runtime.CurrentLevel, mapLoadContext, Coordinator.Port, out string errorMessage))
{
Coordinator.EnterFailureFallback($"Combat loading failed. {errorMessage}");
}
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_ = elapseSeconds;
_ = realElapseSeconds;
if (!Runtime.LoadSession.IsReady)
{
return;
}
Coordinator.TryBeginNextPhase();
}
private MapEntityLoadContext BuildMapLoadContext()
{
List<TowerStatsData> buildTowerStatsSnapshot = new();
for (int i = 0; i < Runtime.CombatRunResourceStore.CurrentBuildTowerCount; i++)
{
if (Runtime.CombatRunResourceStore.TryGetBuildTowerStats(i, out TowerStatsData stats) &&
stats != null)
{
buildTowerStatsSnapshot.Add(stats);
}
}
MapData mapData = new MapData(
entityId: 0,
typeId: 0,
levelId: Runtime.CurrentLevel.Id,
position: Vector3.zero,
initialCoin: Runtime.CombatRunResourceStore.CurrentCoin,
buildTowerStatsSnapshot: buildTowerStatsSnapshot,
inventorySnapshot: Runtime.CombatRunResourceStore.GetCombatInventorySnapshot(),
participantTowerSnapshot: Runtime.CombatRunResourceStore.GetParticipantTowerSnapshot());
return new MapEntityLoadContext(
mapData,
Runtime.CombatRunResourceStore.TryConsumeCoin,
Runtime.CombatRunResourceStore.AddCoin);
}
}
}
@@ -0,0 +1,43 @@
namespace GeometryTD.CustomComponent
{
internal sealed class CombatRewardSelectionState : CombatStateBase
{
public CombatRewardSelectionState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
: base(runtime, coordinator)
{
}
public override void OnEnter()
{
if (Runtime.SettlementContext == null)
{
Coordinator.EnterFailureFallback("Combat reward selection failed. Settlement context is missing.");
return;
}
Coordinator.EnsureRewardSelectFormUseCaseBound();
int nextRewardOrdinal = Runtime.NextRewardOrdinal;
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
Runtime.SettlementContext,
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
Coordinator.ResolveCurrentThemeType(),
Runtime.NodeContext != null ? Runtime.NodeContext.RunSeed : 0,
Runtime.NodeContext != null ? Runtime.NodeContext.SequenceIndex : -1,
ref nextRewardOrdinal,
Runtime.RewardSelectFormUseCase,
Coordinator.OnFullBaseHpRewardSelected,
Coordinator.OnFullBaseHpRewardGiveUp))
{
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
return;
}
Runtime.NextRewardOrdinal = nextRewardOrdinal;
}
public override void OnExit()
{
Coordinator.CloseRewardSelectForm();
}
}
}
@@ -0,0 +1,82 @@
using System.Collections.Generic;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Procedure;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatRunningPhaseState : CombatStateBase
{
private readonly DRLevelPhase _phase;
private readonly IReadOnlyList<DRLevelSpawnEntry> _spawnEntries;
public CombatRunningPhaseState(
CombatSchedulerRuntime runtime,
CombatSchedulerCoordinator coordinator,
DRLevelPhase phase,
IReadOnlyList<DRLevelSpawnEntry> spawnEntries) : base(runtime, coordinator)
{
_phase = phase;
_spawnEntries = spawnEntries;
}
public override void OnEnter()
{
Runtime.EnemyManager.BeginPhase(_phase, _spawnEntries);
GameEntry.Event.Fire(
Coordinator,
CombatProcessEventArgs.Create(
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
Runtime.PhaseLoopRuntime.PhaseCount));
GameEntry.Event.Fire(
Coordinator,
CombatEnemyHpRateChangedEventArgs.Create(
Coordinator.ResolveEnemyHpRateMultiplier(
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
Runtime.PhaseLoopRuntime.PhaseCount)));
if (!Runtime.NodeEnterFired)
{
Runtime.NodeEnterFired = true;
GameEntry.Event.Fire(
Coordinator,
NodeEnterEventArgs.Create(
Runtime.NodeContext?.RunId,
Runtime.NodeContext?.NodeId ?? 0,
Runtime.NodeContext?.NodeType ?? RunNodeType.None,
Runtime.NodeContext?.SequenceIndex ?? -1));
}
Log.Info(
"CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.",
Runtime.CurrentLevel != null ? Runtime.CurrentLevel.Id : 0,
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
_phase.EndType,
_spawnEntries != null ? _spawnEntries.Count : 0);
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (Runtime.PhaseLoopRuntime.CurrentPhase == null)
{
Coordinator.EnterFailureFallback("CombatScheduler update failed. Current phase is null.");
return;
}
Runtime.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
Runtime.EnemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
if (Coordinator.ShouldEnterSettlementFromActiveState(out bool didCombatWin))
{
Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, didCombatWin));
return;
}
if (Runtime.EnemyManager.IsPhaseSpawnCompleted)
{
Coordinator.EnterWaitingForPhaseEnd();
}
}
}
}
@@ -0,0 +1,34 @@
namespace GeometryTD.CustomComponent
{
internal sealed class CombatSettlementState : CombatStateBase
{
private readonly bool _didCombatWin;
public CombatSettlementState(
CombatSchedulerRuntime runtime,
CombatSchedulerCoordinator coordinator,
bool didCombatWin) : base(runtime, coordinator)
{
_didCombatWin = didCombatWin;
}
public override void OnEnter()
{
Runtime.EnemyManager.EndPhase();
Runtime.EnemyManager.CleanupTrackedEnemies();
Runtime.DidCombatWin = _didCombatWin;
Runtime.SettlementContext = Runtime.CombatSettlementService.BuildSettlementContext(
_didCombatWin,
Runtime.CurrentLevel,
Runtime.EnemyManager.DefeatedEnemyCount,
Runtime.CombatRunResourceStore);
if (Runtime.SettlementContext.Flags.ShouldOpenRewardSelection)
{
Coordinator.ChangeState(new CombatRewardSelectionState(Runtime, Coordinator));
return;
}
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
}
}
}
@@ -0,0 +1,34 @@
namespace GeometryTD.CustomComponent
{
internal abstract class CombatStateBase
{
protected CombatSchedulerRuntime Runtime { get; }
protected CombatSchedulerCoordinator Coordinator { get; }
protected CombatStateBase(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
{
Runtime = runtime;
Coordinator = coordinator;
}
public virtual void OnInit()
{
}
public virtual void OnEnter()
{
}
public virtual void OnExit()
{
}
public virtual void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
}
public virtual void OnDestroy()
{
}
}
}
@@ -0,0 +1,47 @@
using GeometryTD.DataTable;
using GeometryTD.Factory;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatWaitingForPhaseEndState : CombatStateBase
{
public CombatWaitingForPhaseEndState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
: base(runtime, coordinator)
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_ = realElapseSeconds;
DRLevelPhase currentPhase = Runtime.PhaseLoopRuntime.CurrentPhase;
if (currentPhase == null)
{
Coordinator.EnterFailureFallback("CombatScheduler waiting phase failed. Current phase is null.");
return;
}
Runtime.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
if (Coordinator.ShouldEnterSettlementFromActiveState(out bool didCombatWin))
{
Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, didCombatWin));
return;
}
PhaseEndConditionContext conditionContext = new(
currentPhase,
Runtime.PhaseLoopRuntime.CurrentPhaseElapsed,
Runtime.EnemyManager.IsPhaseSpawnCompleted,
Runtime.EnemyManager.AliveEnemyCount,
Runtime.EnemyManager.HasAliveBoss);
IPhaseEndCondition endCondition = PhaseEndConditionFactory.Create(currentPhase.EndType);
if (!endCondition.ShouldExit(conditionContext))
{
return;
}
Coordinator.CompleteCurrentPhase();
}
}
}
@@ -0,0 +1,40 @@
namespace GeometryTD.CustomComponent
{
internal sealed class CombatWaitingForReturnState : CombatStateBase
{
private bool _returnRequested;
public CombatWaitingForReturnState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
: base(runtime, coordinator)
{
}
public void RequestReturn()
{
_returnRequested = true;
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_ = elapseSeconds;
_ = realElapseSeconds;
if (!_returnRequested)
{
return;
}
if (Runtime.SettlementContext == null)
{
Coordinator.EnterFailureFallback("Combat return failed. Settlement context is missing.");
return;
}
Runtime.CombatSettlementService.CommitSettlementInventory(Runtime.SettlementContext);
Runtime.LoadSession.Cleanup();
Coordinator.CloseCombatFinishForm();
Coordinator.CloseRewardSelectForm();
Coordinator.CompleteNormalCombatAndNotify(Runtime.DidCombatWin);
}
}
}
@@ -0,0 +1,21 @@
using GeometryTD.DataTable;
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
public readonly struct EnemyDropContext
{
public EnemyDropContext(DREnemy enemy, int displayPhaseIndex, LevelThemeType themeType)
{
Enemy = enemy;
DisplayPhaseIndex = displayPhaseIndex;
ThemeType = themeType;
}
public DREnemy Enemy { get; }
public int DisplayPhaseIndex { get; }
public LevelThemeType ThemeType { get; }
}
}
@@ -0,0 +1,22 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
public readonly struct EnemyDropResult
{
public static EnemyDropResult Empty => new(0, 0, null);
public EnemyDropResult(int coin, int gold, TowerCompItemData lootItem)
{
Coin = coin;
Gold = gold;
LootItem = lootItem;
}
public int Coin { get; }
public int Gold { get; }
public TowerCompItemData LootItem { get; }
}
}
@@ -0,0 +1,19 @@
using GeometryTD.DataTable;
namespace GeometryTD.CustomComponent
{
internal interface ICombatSchedulerPort
{
DRLevel CurrentLevel { get; }
int DisplayPhaseIndex { get; }
int PhaseCount { get; }
int CurrentCoin { get; }
int CurrentBaseHp { get; }
bool CanEndCombat { get; }
void ChangeState(CombatStateBase nextState);
bool TryEndCombatByPlayer();
bool TryDebugFail(string errorMessage);
bool OnCombatFinishReturnRequested();
}
}
@@ -0,0 +1,14 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class BossDeadPhaseEndCondition : IPhaseEndCondition
{
public PhaseEndType EndType => PhaseEndType.BossDead;
public bool ShouldExit(in PhaseEndConditionContext context)
{
return context.IsPhaseSpawnCompleted && !context.HasAliveBoss;
}
}
}
@@ -0,0 +1,14 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemiesClearedPhaseEndCondition : IPhaseEndCondition
{
public PhaseEndType EndType => PhaseEndType.EnemiesCleared;
public bool ShouldExit(in PhaseEndConditionContext context)
{
return context.IsPhaseSpawnCompleted && context.AliveEnemyCount <= 0;
}
}
}
@@ -0,0 +1,11 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal interface IPhaseEndCondition
{
PhaseEndType EndType { get; }
bool ShouldExit(in PhaseEndConditionContext context);
}
}
@@ -0,0 +1,25 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class NonePhaseEndCondition : IPhaseEndCondition
{
public PhaseEndType EndType => PhaseEndType.None;
public bool ShouldExit(in PhaseEndConditionContext context)
{
if (context.Phase == null)
{
return false;
}
if (context.Phase.DurationSeconds > 0 &&
context.PhaseElapsedSeconds >= context.Phase.DurationSeconds)
{
return true;
}
return context.IsPhaseSpawnCompleted && context.AliveEnemyCount <= 0;
}
}
}
@@ -0,0 +1,31 @@
using GeometryTD.DataTable;
namespace GeometryTD.CustomComponent
{
internal readonly struct PhaseEndConditionContext
{
public PhaseEndConditionContext(
DRLevelPhase phase,
float phaseElapsedSeconds,
bool isPhaseSpawnCompleted,
int aliveEnemyCount,
bool hasAliveBoss)
{
Phase = phase;
PhaseElapsedSeconds = phaseElapsedSeconds;
IsPhaseSpawnCompleted = isPhaseSpawnCompleted;
AliveEnemyCount = aliveEnemyCount;
HasAliveBoss = hasAliveBoss;
}
public DRLevelPhase Phase { get; }
public float PhaseElapsedSeconds { get; }
public bool IsPhaseSpawnCompleted { get; }
public int AliveEnemyCount { get; }
public bool HasAliveBoss { get; }
}
}
@@ -0,0 +1,37 @@
using System.Globalization;
using GeometryTD.DataTable;
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class TimeElapsedPhaseEndCondition : IPhaseEndCondition
{
public PhaseEndType EndType => PhaseEndType.TimeElapsed;
public bool ShouldExit(in PhaseEndConditionContext context)
{
if (context.Phase == null)
{
return false;
}
return context.PhaseElapsedSeconds >= ResolveTimeElapsedThreshold(context.Phase);
}
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,101 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
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 bool IsEndCombatRequested => _endCombatRequested;
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 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;
}
}
}
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public sealed class CombatSettlementCalculator
{
private const float FullBaseHpGoldBonusRate = 0.3f;
private const float HighBaseHpGoldBonusRate = 0.1f;
private const float HighBaseHpThreshold = 0.8f;
private const float SettlementTowerEnduranceLoss = 1f;
public CombatSettlementContext BuildSettlementContext(
bool didCombatWin,
DRLevel currentLevel,
int defeatedEnemyCount,
CombatRunResourceStore resourceStore)
{
bool shouldOpenFullBaseHpRewardSelect = false;
ResolveSettlementByBaseHp(
didCombatWin,
currentLevel,
resourceStore,
out int currentBaseHp,
out int maxBaseHp,
out int levelRewardGold,
out float bonusRate,
out int bonusGold,
out shouldOpenFullBaseHpRewardSelect);
CombatSettlementContext settlementContext = new CombatSettlementContext();
settlementContext.Result.DidCombatWin = didCombatWin;
settlementContext.Result.FinalCoin = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentCoin : 0);
settlementContext.Result.FinalBaseHp = currentBaseHp;
settlementContext.Result.MaxBaseHp = maxBaseHp;
settlementContext.Result.DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount);
settlementContext.Result.GainedGold = Mathf.Max(0, resourceStore != null ? resourceStore.GainedGold : 0);
settlementContext.Result.RewardInventory = resourceStore != null
? resourceStore.GetRewardInventorySnapshot()
: new BackpackInventoryData();
PopulateEnduranceSettlement(settlementContext, resourceStore);
settlementContext.Flags.ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect;
settlementContext.Flags.DidEnterRewardSelection = false;
settlementContext.Summary.DefeatedEnemyCount = settlementContext.Result.DefeatedEnemyCount;
settlementContext.Summary.GainedGold = settlementContext.Result.GainedGold;
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
Log.Info(
"Combat settlement resolved. Level={0}, BaseHp={1}/{2}, LevelReward={3}, BonusRate={4:P0}, BonusGold={5}, FullHpRewardSelect={6}, EnduranceTargets={7}.",
currentLevel != null ? currentLevel.Id : 0,
currentBaseHp,
maxBaseHp,
levelRewardGold,
bonusRate,
bonusGold,
shouldOpenFullBaseHpRewardSelect,
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count);
return settlementContext;
}
private static void ResolveSettlementByBaseHp(
bool didCombatWin,
DRLevel currentLevel,
CombatRunResourceStore resourceStore,
out int currentBaseHp,
out int maxBaseHp,
out int levelRewardGold,
out float bonusRate,
out int bonusGold,
out bool shouldOpenFullBaseHpRewardSelect)
{
currentBaseHp = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentBaseHp : 0);
maxBaseHp = resourceStore != null ? Mathf.Max(0, resourceStore.MaxBaseHp) : 0;
if (maxBaseHp > 0)
{
currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp);
}
levelRewardGold = currentLevel != null ? Mathf.Max(0, currentLevel.RewardGold) : 0;
bonusRate = 0f;
bonusGold = 0;
shouldOpenFullBaseHpRewardSelect = false;
if (!didCombatWin || resourceStore == null)
{
return;
}
if (maxBaseHp > 0 && currentBaseHp >= maxBaseHp)
{
bonusRate = FullBaseHpGoldBonusRate;
shouldOpenFullBaseHpRewardSelect = true;
}
else if (maxBaseHp > 0)
{
float hpRate = (float)currentBaseHp / maxBaseHp;
if (hpRate >= HighBaseHpThreshold)
{
bonusRate = HighBaseHpGoldBonusRate;
}
}
int goldForBonusCalculation = Mathf.Max(0, resourceStore.GainedGold) + levelRewardGold;
bonusGold = bonusRate > 0f ? Mathf.FloorToInt(goldForBonusCalculation * bonusRate) : 0;
int settlementGold = levelRewardGold + bonusGold;
resourceStore.AddSettlementGold(settlementGold);
}
private static void PopulateEnduranceSettlement(
CombatSettlementContext settlementContext,
CombatRunResourceStore resourceStore)
{
if (settlementContext == null)
{
return;
}
CombatSettlementEnduranceResult enduranceResult = settlementContext.Result.Endurance;
enduranceResult.TargetTowerInstanceIds.Clear();
enduranceResult.EnduranceLossPerComponent = SettlementTowerEnduranceLoss;
IReadOnlyList<long> participantTowerIds = resourceStore?.GetParticipantTowerInstanceIdSnapshot();
if (participantTowerIds == null || participantTowerIds.Count <= 0)
{
return;
}
for (int i = 0; i < participantTowerIds.Count; i++)
{
long towerId = participantTowerIds[i];
if (towerId > 0)
{
enduranceResult.TargetTowerInstanceIds.Add(towerId);
}
}
}
}
}
@@ -0,0 +1,83 @@
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public sealed class CombatSettlementCommitter
{
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
{
if (settlementContext == null || settlementContext.Flags.IsCommitted)
{
return;
}
BackpackInventoryData rewardInventory = settlementContext.Result.RewardInventory ?? new BackpackInventoryData();
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
settlementContext.Result.RewardInventory = rewardInventory;
settlementContext.Summary.RewardInventory = rewardInventory;
settlementContext.Result.Endurance.AffectedTowerCount = ApplyDeferredSettlementEndurance(settlementContext);
settlementContext.Flags.IsCommitted = true;
}
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
{
if (settlementContext?.Result.RewardInventory == null || selectedReward?.SourceItem == null)
{
return;
}
TryAppendRewardComponent(settlementContext.Result.RewardInventory, selectedReward.SourceItem);
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
}
private static int ApplyDeferredSettlementEndurance(CombatSettlementContext settlementContext)
{
if (settlementContext == null ||
settlementContext.Result.Endurance.EnduranceLossPerComponent <= 0f ||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count <= 0)
{
return 0;
}
PlayerInventoryComponent inventory = GameEntry.PlayerInventory;
if (inventory == null)
{
return 0;
}
return inventory.ReduceTowerEndurance(
settlementContext.Result.Endurance.TargetTowerInstanceIds,
settlementContext.Result.Endurance.EnduranceLossPerComponent);
}
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;
}
}
}
@@ -0,0 +1,104 @@
using System;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public sealed class EnemyConfigProvider
{
private const int DefaultEnemyConfigId = 1;
private IDataTable<DREnemy> _drEnemy;
private bool _enemyConfigMissingLogged;
public void Reset()
{
_drEnemy = null;
_enemyConfigMissingLogged = false;
}
public DREnemy GetEnemyConfig(int enemyId)
{
if (_drEnemy == null)
{
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
if (_drEnemy == null)
{
if (!_enemyConfigMissingLogged)
{
Log.Warning("EnemyConfigProvider can not find DREnemy data table.");
_enemyConfigMissingLogged = true;
}
return null;
}
}
if (enemyId > 0)
{
DREnemy targetConfig = _drEnemy.GetDataRow(enemyId);
if (targetConfig != null)
{
return targetConfig;
}
}
DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId);
if (defaultConfig != null)
{
return defaultConfig;
}
DREnemy[] allConfigs = _drEnemy.GetAllDataRows();
if (allConfigs.Length > 0)
{
return allConfigs[0];
}
if (!_enemyConfigMissingLogged)
{
Log.Warning("EnemyConfigProvider found no enemy configs.");
_enemyConfigMissingLogged = true;
}
return null;
}
public int ResolveScaledEnemyBaseHp(int baseHp, CombatScheduler combatScheduler)
{
int resolvedBaseHp = Mathf.Max(1, baseHp);
int completedLoopCount = ResolveCompletedLoopCount(combatScheduler);
if (completedLoopCount <= 0)
{
return resolvedBaseHp;
}
double scaled = resolvedBaseHp * Math.Pow(2d, completedLoopCount);
if (scaled >= int.MaxValue)
{
return int.MaxValue;
}
return Math.Max(1, (int)Math.Round(scaled));
}
private static int ResolveCompletedLoopCount(CombatScheduler combatScheduler)
{
if (combatScheduler == null)
{
return 0;
}
int phaseCount = combatScheduler.PhaseCount;
int displayPhaseIndex = combatScheduler.DisplayPhaseIndex;
if (phaseCount <= 0 || displayPhaseIndex <= 0)
{
return 0;
}
return Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount);
}
}
}
@@ -0,0 +1,91 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemyLifecycleTracker
{
private readonly HashSet<int> _aliveBossEntityIds = new();
private readonly HashSet<int> _trackedEnemyEntityIds = new();
private readonly Dictionary<int, DREnemy> _trackedEnemyConfigByEntityId = new();
private readonly Dictionary<int, bool> _bossFlagsByEntityId = new();
public int AliveEnemyCount { get; private set; }
public bool HasAliveBoss => _aliveBossEntityIds.Count > 0;
public void Reset()
{
_aliveBossEntityIds.Clear();
_bossFlagsByEntityId.Clear();
_trackedEnemyEntityIds.Clear();
_trackedEnemyConfigByEntityId.Clear();
AliveEnemyCount = 0;
}
public void TrackEnemy(int entityId, DREnemy enemyConfig, bool isBoss)
{
_trackedEnemyEntityIds.Add(entityId);
_trackedEnemyConfigByEntityId[entityId] = enemyConfig;
_bossFlagsByEntityId[entityId] = isBoss;
}
public bool Contains(int entityId)
{
return _trackedEnemyEntityIds.Contains(entityId);
}
public void HandleShowSuccess(int entityId)
{
if (_trackedEnemyEntityIds.Contains(entityId))
{
AliveEnemyCount++;
if (_bossFlagsByEntityId.TryGetValue(entityId, out bool isBoss) && isBoss)
{
_aliveBossEntityIds.Add(entityId);
}
}
}
public void HandleShowFailure(int entityId)
{
_aliveBossEntityIds.Remove(entityId);
_bossFlagsByEntityId.Remove(entityId);
_trackedEnemyEntityIds.Remove(entityId);
_trackedEnemyConfigByEntityId.Remove(entityId);
}
public bool TryHandleHideComplete(int entityId, out DREnemy enemyConfig)
{
enemyConfig = null;
if (!_trackedEnemyEntityIds.Remove(entityId))
{
_aliveBossEntityIds.Remove(entityId);
_bossFlagsByEntityId.Remove(entityId);
_trackedEnemyConfigByEntityId.Remove(entityId);
return false;
}
_trackedEnemyConfigByEntityId.TryGetValue(entityId, out enemyConfig);
_aliveBossEntityIds.Remove(entityId);
_bossFlagsByEntityId.Remove(entityId);
_trackedEnemyConfigByEntityId.Remove(entityId);
AliveEnemyCount = Mathf.Max(0, AliveEnemyCount - 1);
return true;
}
public void CopyTrackedEntityIdsTo(List<int> buffer)
{
if (buffer == null)
{
return;
}
buffer.Clear();
foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds)
{
buffer.Add(trackedEnemyEntityId);
}
}
}
}
@@ -0,0 +1,223 @@
using System.Collections.Generic;
using GameFramework.Event;
using GeometryTD.DataTable;
using GeometryTD.Entity;
using GeometryTD.Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public class EnemyManager
{
private readonly List<int> _trackedEnemyIdBuffer = new();
private readonly EnemySpawnDirector _enemySpawnDirector = new();
private readonly EnemyConfigProvider _enemyConfigProvider = new();
private readonly EnemySpawnPathResolver _enemySpawnPathResolver = new();
private readonly EnemyLifecycleTracker _enemyLifecycleTracker = new();
private CombatScheduler _combatScheduler;
private EntityComponent _entity;
private int _defeatedEnemyCount;
private bool _initialized;
public int AliveEnemyCount => _enemyLifecycleTracker.AliveEnemyCount;
public int DefeatedEnemyCount => _defeatedEnemyCount;
public bool HasAliveBoss => _enemyLifecycleTracker.HasAliveBoss;
public bool IsPhaseSpawnCompleted => _enemySpawnDirector.IsPhaseSpawnCompleted;
public bool IsPhaseRunning => _enemySpawnDirector.IsPhaseRunning;
public void OnInit(CombatScheduler combatScheduler)
{
_combatScheduler = combatScheduler;
if (_initialized)
{
return;
}
_entity = GameEntry.Entity;
_defeatedEnemyCount = 0;
_enemySpawnDirector.Reset();
_enemyConfigProvider.Reset();
_enemySpawnPathResolver.Reset();
_trackedEnemyIdBuffer.Clear();
_enemyLifecycleTracker.Reset();
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_initialized = true;
}
public void BeginPhase(DRLevelPhase phase, IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
{
if (!_initialized || _combatScheduler == null)
{
return;
}
_ = phase;
EndPhase();
_enemySpawnPathResolver.RefreshCache(_combatScheduler, true);
_enemySpawnDirector.BeginPhase(spawnEntries);
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_initialized || _combatScheduler == null || !_enemySpawnDirector.IsPhaseRunning)
{
return;
}
_enemySpawnPathResolver.RefreshCache(_combatScheduler, false);
_enemySpawnDirector.OnUpdate(elapseSeconds, SpawnEnemies);
}
public void EndPhase()
{
_enemySpawnDirector.EndPhase();
}
public void OnDestroy()
{
if (!_initialized)
{
_combatScheduler = null;
return;
}
CleanupTrackedEnemies();
EndPhase();
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_enemySpawnPathResolver.Reset();
_trackedEnemyIdBuffer.Clear();
_enemyLifecycleTracker.Reset();
_defeatedEnemyCount = 0;
_enemyConfigProvider.Reset();
_combatScheduler = null;
_initialized = false;
}
public void ResetCombatStats()
{
_defeatedEnemyCount = 0;
}
public void CleanupTrackedEnemies()
{
_enemyLifecycleTracker.CopyTrackedEntityIdsTo(_trackedEnemyIdBuffer);
if (_trackedEnemyIdBuffer.Count <= 0)
{
return;
}
_enemyLifecycleTracker.Reset();
if (_entity == null)
{
return;
}
for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++)
{
int trackedEnemyEntityId = _trackedEnemyIdBuffer[i];
if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId))
{
_entity.HideEntity(trackedEnemyEntityId);
}
}
}
private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount)
{
if (spawnCount <= 0)
{
return;
}
if (!_enemySpawnPathResolver.TryResolveSpawnPath(_combatScheduler, entry.SpawnPointId, out IReadOnlyList<Vector3> pathPoints))
{
return;
}
DREnemy enemyConfig = _enemyConfigProvider.GetEnemyConfig(entry.EnemyId);
if (enemyConfig == null)
{
return;
}
int scaledBaseHp = _enemyConfigProvider.ResolveScaledEnemyBaseHp(enemyConfig.BaseHp, _combatScheduler);
bool isBoss = entry.EntryType == Definition.EntryType.Boss;
for (int i = 0; i < spawnCount; i++)
{
int enemyEntityId = _entity.GenerateSerialId();
_enemyLifecycleTracker.TrackEnemy(enemyEntityId, enemyConfig, isBoss);
EnemyData enemyData = new EnemyData(
enemyEntityId,
enemyConfig.EntityId,
pathPoints[0],
scaledBaseHp,
enemyConfig.Speed,
pathPoints);
_entity.ShowEnemy(enemyData);
}
}
private void OnShowEntitySuccess(object sender, GameEventArgs e)
{
if (!(e is ShowEntitySuccessEventArgs ne)) return;
if (ne.EntityLogicType == typeof(EnemyEntity) &&
_enemyLifecycleTracker.Contains(ne.Entity.Id))
{
_enemyLifecycleTracker.HandleShowSuccess(ne.Entity.Id);
}
}
private void OnShowEntityFailure(object sender, GameEventArgs e)
{
if (!(e is ShowEntityFailureEventArgs ne))
{
return;
}
if (ne.EntityLogicType != typeof(EnemyEntity))
{
return;
}
_enemyLifecycleTracker.HandleShowFailure(ne.EntityId);
}
private void OnHideEntityComplete(object sender, GameEventArgs e)
{
if (!(e is HideEntityCompleteEventArgs ne))
{
return;
}
if (!_enemyLifecycleTracker.TryHandleHideComplete(ne.EntityId, out DREnemy enemyConfig))
{
return;
}
bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId);
bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning;
if (isCombatRunning && wasKilled && enemyConfig != null)
{
_defeatedEnemyCount++;
_combatScheduler.OnEnemyDefeated(enemyConfig);
}
else if (isCombatRunning && !wasKilled && enemyConfig != null)
{
_combatScheduler.OnEnemyReachedBase(enemyConfig);
}
}
}
}
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemySpawnDirector
{
private sealed class SpawnEntryRuntime
{
public DRLevelSpawnEntry Entry;
public bool Completed;
public float NextTriggerTime;
public float EndTime;
public int RemainingCount;
}
private const float MinStreamInterval = 0.05f;
private const float MinBurstGap = 0.01f;
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new();
private float _phaseElapsed;
public bool IsPhaseSpawnCompleted { get; private set; } = true;
public bool IsPhaseRunning { get; private set; }
public void Reset()
{
EndPhase();
}
public void BeginPhase(IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
{
EndPhase();
_phaseElapsed = 0f;
IsPhaseRunning = true;
IsPhaseSpawnCompleted = false;
if (spawnEntries != null)
{
for (int i = 0; i < spawnEntries.Count; i++)
{
SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]);
if (runtime != null)
{
_spawnRuntimes.Add(runtime);
}
}
}
IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0;
}
public void OnUpdate(float elapseSeconds, Action<DRLevelSpawnEntry, int> spawnAction)
{
if (!IsPhaseRunning || spawnAction == null)
{
return;
}
_phaseElapsed += elapseSeconds;
UpdateSpawnRuntimes(spawnAction);
}
public void EndPhase()
{
IsPhaseRunning = false;
_phaseElapsed = 0f;
_spawnRuntimes.Clear();
IsPhaseSpawnCompleted = true;
}
private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry)
{
if (entry == null || entry.EntryType == EntryType.None)
{
return null;
}
SpawnEntryRuntime runtime = new SpawnEntryRuntime
{
Entry = entry,
Completed = false,
NextTriggerTime = Mathf.Max(0f, entry.StartTime),
EndTime = Mathf.Max(0f, entry.StartTime),
RemainingCount = Mathf.Max(0, entry.Count)
};
switch (entry.EntryType)
{
case EntryType.Stream:
{
float duration = Mathf.Max(0f, entry.Duration);
runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime;
runtime.Completed = entry.Count <= 0;
return runtime;
}
case EntryType.Burst:
case EntryType.Boss:
runtime.Completed = runtime.RemainingCount <= 0;
return runtime;
default:
return null;
}
}
private void UpdateSpawnRuntimes(Action<DRLevelSpawnEntry, int> spawnAction)
{
bool allCompleted = true;
for (int i = 0; i < _spawnRuntimes.Count; i++)
{
SpawnEntryRuntime runtime = _spawnRuntimes[i];
if (runtime.Completed)
{
continue;
}
switch (runtime.Entry.EntryType)
{
case EntryType.Stream:
ProcessStreamRuntime(runtime, spawnAction);
break;
case EntryType.Burst:
case EntryType.Boss:
ProcessBurstRuntime(runtime, spawnAction);
break;
default:
runtime.Completed = true;
break;
}
if (!runtime.Completed)
{
allCompleted = false;
}
}
IsPhaseSpawnCompleted = allCompleted;
}
private void ProcessStreamRuntime(SpawnEntryRuntime runtime, Action<DRLevelSpawnEntry, int> spawnAction)
{
if (_phaseElapsed < runtime.NextTriggerTime)
{
return;
}
int countPerWave = Mathf.Max(0, runtime.Entry.Count);
if (countPerWave <= 0)
{
runtime.Completed = true;
return;
}
float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval;
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime)
{
spawnAction(runtime.Entry, countPerWave);
runtime.NextTriggerTime += interval;
}
if (runtime.NextTriggerTime > runtime.EndTime)
{
runtime.Completed = true;
}
}
private void ProcessBurstRuntime(SpawnEntryRuntime runtime, Action<DRLevelSpawnEntry, int> spawnAction)
{
if (_phaseElapsed < runtime.NextTriggerTime)
{
return;
}
if (runtime.RemainingCount <= 0)
{
runtime.Completed = true;
return;
}
float gap = runtime.Entry.Gap;
if (gap <= 0f)
{
spawnAction(runtime.Entry, runtime.RemainingCount);
runtime.RemainingCount = 0;
runtime.Completed = true;
return;
}
gap = Mathf.Max(gap, MinBurstGap);
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0)
{
spawnAction(runtime.Entry, 1);
runtime.RemainingCount--;
runtime.NextTriggerTime += gap;
}
runtime.Completed = runtime.RemainingCount <= 0;
}
}
}
@@ -0,0 +1,110 @@
using System.Collections.Generic;
using GeometryTD.Entity;
using GeometryTD.Map;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemySpawnPathResolver
{
private readonly List<Spawner> _spawners = new();
private readonly Dictionary<int, Spawner> _spawnerByOrder = new();
private readonly List<Vector3> _pathBuffer = new();
private int _nextSpawnerIndex;
private int _currentMapEntityId;
public void Reset()
{
_spawners.Clear();
_spawnerByOrder.Clear();
_pathBuffer.Clear();
_nextSpawnerIndex = 0;
_currentMapEntityId = 0;
}
public void RefreshCache(CombatScheduler combatScheduler, bool force)
{
MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null;
if (currentMap == null)
{
Reset();
return;
}
if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0)
{
return;
}
_spawners.Clear();
_spawnerByOrder.Clear();
_nextSpawnerIndex = 0;
_currentMapEntityId = currentMap.Id;
Spawner[] mapSpawners = currentMap.Spawners;
for (int i = 0; i < mapSpawners.Length; i++)
{
Spawner spawner = mapSpawners[i];
if (spawner == null)
{
continue;
}
if (!currentMap.TryGetDefaultPathCells(spawner, out _))
{
continue;
}
_spawners.Add(spawner);
if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder))
{
_spawnerByOrder[spawner.SpawnOrder] = spawner;
}
}
_spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder));
}
public bool TryResolveSpawnPath(CombatScheduler combatScheduler, int spawnPointId, out IReadOnlyList<Vector3> pathPoints)
{
pathPoints = null;
MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null;
if (currentMap == null)
{
return false;
}
Spawner spawner = ResolveSpawner(spawnPointId);
if (spawner == null)
{
return false;
}
if (!currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) || _pathBuffer.Count <= 0)
{
return false;
}
pathPoints = _pathBuffer;
return true;
}
private Spawner ResolveSpawner(int spawnPointId)
{
if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner))
{
return mappedSpawner;
}
if (_spawners.Count <= 0)
{
return null;
}
Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count];
_nextSpawnerIndex++;
return fallbackSpawner;
}
}
}