补全 CombatNode 逻辑 + 重构 EnemyManager

- 添加了关卡内的难度系数与掉落
    - 难度系数:每循环一轮波次敌人血量翻倍
    - 道具掉落:按照掉落概率曲线根据波次计算当前爆率,并从新增的道具池里选择道具
    - `CombatResourceManager`:统一维护关卡内资源状态(硬币/金币/道具掉落)
- 重构 `EnemyManager`:
    - `EnemyManager`:编排子服务,不承载具体业务细节
    - `EnemySpawnDirector`:管理刷怪时序
    - `SpawnerResolver`:管理出生点与路径
    - `EnemyLifecycleTracker`:追踪敌人生命周期
    - `EnemyConfigService`:管理敌人配置与倍率
This commit is contained in:
SepComet
2026-03-02 17:23:34 +08:00
parent c576224991
commit 5ba94828a8
49 changed files with 1763 additions and 1047 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,277 @@
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, 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 (_combatInfoFormUseCase == null)
{
_combatInfoFormUseCase = new CombatInfoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _combatInfoFormUseCase);
}
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:
@@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
using Random = UnityEngine.Random;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatResourceManager
{
private const float DropChanceBase = 0.05f;
private const float DropChancePerPhase = 0.0125f;
private const float DropChanceCap = 0.45f;
private const float RarityCurveScalePhase = 30f;
private readonly List<DROutGameDropPool> _eligibleDropPoolBuffer = new();
private readonly Dictionary<RarityType, float> _rarityRollWeightBuffer = new();
private readonly BackpackInventoryData _rewardInventory = new();
private IDataTable<DROutGameDropPool> _drOutGameDropPool;
private IDataTable<DRMuzzleComp> _drMuzzleComp;
private IDataTable<DRBearingComp> _drBearingComp;
private IDataTable<DRBaseComp> _drBaseComp;
private long _nextDropItemInstanceId = 1;
public int GainedCoin { get; private set; }
public int GainedGold { get; private set; }
public void Reset()
{
GainedCoin = 0;
GainedGold = 0;
_rewardInventory.Gold = 0;
_rewardInventory.MuzzleComponents.Clear();
_rewardInventory.BearingComponents.Clear();
_rewardInventory.BaseComponents.Clear();
_rewardInventory.Towers.Clear();
_nextDropItemInstanceId = 1;
}
public BackpackInventoryData GetRewardInventorySnapshot()
{
return new BackpackInventoryData
{
Gold = _rewardInventory.Gold,
MuzzleComponents = new List<MuzzleCompItemData>(_rewardInventory.MuzzleComponents),
BearingComponents = new List<BearingCompItemData>(_rewardInventory.BearingComponents),
BaseComponents = new List<BaseCompItemData>(_rewardInventory.BaseComponents),
Towers = new List<DefenseTowerItemData>(_rewardInventory.Towers)
};
}
public void AddEnemyDefeatedReward(int gainedCoin, int gainedGold)
{
int coin = Mathf.Max(0, gainedCoin);
int gold = Mathf.Max(0, gainedGold);
GainedCoin += coin;
GainedGold += gold;
if (gold > 0)
{
_rewardInventory.Gold += gold;
}
GameEntry.CombatNode?.ApplyEnemyDropReward(coin, gold);
}
public bool TryRollOutGameItemDrop(int displayPhaseIndex, LevelThemeType themeType)
{
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
if (Random.value > dropChance)
{
return false;
}
if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow))
{
return false;
}
if (!TryBuildDropItem(selectedRow, out TowerCompItemData droppedItem))
{
return false;
}
if (droppedItem is MuzzleCompItemData muzzleCompItemData)
{
_rewardInventory.MuzzleComponents.Add(muzzleCompItemData);
return true;
}
if (droppedItem is BearingCompItemData bearingCompItemData)
{
_rewardInventory.BearingComponents.Add(bearingCompItemData);
return true;
}
if (droppedItem is BaseCompItemData baseCompItemData)
{
_rewardInventory.BaseComponents.Add(baseCompItemData);
return true;
}
return false;
}
private bool TryPickDropPoolRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow)
{
selectedRow = null;
IDataTable<DROutGameDropPool> dropTable = EnsureOutGameDropPoolTable();
if (dropTable == null)
{
return false;
}
_eligibleDropPoolBuffer.Clear();
DROutGameDropPool[] allRows = dropTable.GetAllDataRows();
for (int i = 0; i < allRows.Length; i++)
{
DROutGameDropPool row = allRows[i];
if (row == null)
{
continue;
}
if (row.LevelThemeType != themeType)
{
continue;
}
if (displayPhaseIndex < row.MinPhase || displayPhaseIndex > row.MaxPhase)
{
continue;
}
_eligibleDropPoolBuffer.Add(row);
}
if (_eligibleDropPoolBuffer.Count <= 0)
{
return false;
}
RarityType selectedRarity = RollRarity(displayPhaseIndex, _eligibleDropPoolBuffer);
if (selectedRarity == RarityType.None)
{
return false;
}
int totalWeight = 0;
for (int i = 0; i < _eligibleDropPoolBuffer.Count; i++)
{
DROutGameDropPool row = _eligibleDropPoolBuffer[i];
if (row.Rarity != selectedRarity)
{
continue;
}
totalWeight += Mathf.Max(1, row.Weight);
}
if (totalWeight <= 0)
{
return false;
}
int randomWeight = Random.Range(1, totalWeight + 1);
int cumulativeWeight = 0;
for (int i = 0; i < _eligibleDropPoolBuffer.Count; i++)
{
DROutGameDropPool row = _eligibleDropPoolBuffer[i];
if (row.Rarity != selectedRarity)
{
continue;
}
cumulativeWeight += Mathf.Max(1, row.Weight);
if (randomWeight <= cumulativeWeight)
{
selectedRow = row;
return true;
}
}
selectedRow = _eligibleDropPoolBuffer[_eligibleDropPoolBuffer.Count - 1];
return selectedRow != null;
}
private RarityType RollRarity(int displayPhaseIndex, List<DROutGameDropPool> candidates)
{
_rarityRollWeightBuffer.Clear();
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
for (int i = 0; i < candidates.Count; i++)
{
DROutGameDropPool row = candidates[i];
if (row == null)
{
continue;
}
float curveWeight = GetRarityCurveWeight(row.Rarity, phaseT);
if (curveWeight <= 0f)
{
continue;
}
if (_rarityRollWeightBuffer.TryGetValue(row.Rarity, out float existingWeight))
{
_rarityRollWeightBuffer[row.Rarity] = existingWeight + Mathf.Max(1, row.Weight) * curveWeight;
}
else
{
_rarityRollWeightBuffer[row.Rarity] = Mathf.Max(1, row.Weight) * curveWeight;
}
}
float totalWeight = 0f;
foreach (var pair in _rarityRollWeightBuffer)
{
totalWeight += Mathf.Max(0f, pair.Value);
}
if (totalWeight <= 0f)
{
return RarityType.None;
}
float randomWeight = Random.value * totalWeight;
float cumulativeWeight = 0f;
foreach (var pair in _rarityRollWeightBuffer)
{
cumulativeWeight += Mathf.Max(0f, pair.Value);
if (randomWeight <= cumulativeWeight)
{
return pair.Key;
}
}
foreach (var pair in _rarityRollWeightBuffer)
{
return pair.Key;
}
return RarityType.None;
}
private static float GetRarityCurveWeight(RarityType rarityType, float phaseT)
{
float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f));
switch (rarityType)
{
case RarityType.White:
return Mathf.Max(0.05f, 0.18f + 1.25f * hump);
case RarityType.Green:
return Mathf.Max(0.05f, 0.35f + 0.55f * hump);
case RarityType.Blue:
return 0.18f + 0.55f * phaseT;
case RarityType.Purple:
return 0.05f + 0.22f * phaseT;
case RarityType.Red:
return 0.01f + 0.08f * phaseT * phaseT;
default:
return 0f;
}
}
private IDataTable<DROutGameDropPool> EnsureOutGameDropPoolTable()
{
if (_drOutGameDropPool != null)
{
return _drOutGameDropPool;
}
_drOutGameDropPool = GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
return _drOutGameDropPool;
}
private bool TryBuildDropItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
if (row == null || row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType))
{
return false;
}
string itemType = row.ItemType.Trim();
if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildMuzzleCompItem(row, out droppedItem);
}
if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildBearingCompItem(row, out droppedItem);
}
if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildBaseCompItem(row, out droppedItem);
}
return false;
}
private bool TryBuildMuzzleCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
_drMuzzleComp ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
if (_drMuzzleComp == null)
{
return false;
}
DRMuzzleComp config = _drMuzzleComp.GetDataRow(row.ItemId);
if (config == null)
{
return false;
}
droppedItem = new MuzzleCompItemData
{
InstanceId = _nextDropItemInstanceId++,
ConfigId = config.Id,
Name = config.Name,
Rarity = row.Rarity,
Endurance = 100f,
Constraint = config.Constraint,
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType
};
return true;
}
private bool TryBuildBearingCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
if (_drBearingComp == null)
{
return false;
}
DRBearingComp config = _drBearingComp.GetDataRow(row.ItemId);
if (config == null)
{
return false;
}
droppedItem = new BearingCompItemData
{
InstanceId = _nextDropItemInstanceId++,
ConfigId = config.Id,
Name = config.Name,
Rarity = row.Rarity,
Endurance = 100f,
Constraint = config.Constraint,
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
};
return true;
}
private bool TryBuildBaseCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
if (_drBaseComp == null)
{
return false;
}
DRBaseComp config = _drBaseComp.GetDataRow(row.ItemId);
if (config == null)
{
return false;
}
droppedItem = new BaseCompItemData
{
InstanceId = _nextDropItemInstanceId++,
ConfigId = config.Id,
Name = config.Name,
Rarity = row.Rarity,
Endurance = 100f,
Constraint = config.Constraint,
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType
};
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 650463d0d65211e4e969ea0d469516d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,481 @@
using System.Collections.Generic;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public class CombatScheduler
{
private enum SchedulerState : byte
{
Idle = 0,
WaitingForLoading = 1,
RunningPhase = 2,
WaitingForFinishReturn = 3,
Completed = 4,
Failed = 5
}
private readonly List<DRLevelPhase> _phaseBuffer = new();
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
private readonly EnemyManager _enemyManager = new();
private readonly PhaseLoopRuntime _phaseLoopRuntime = new();
private readonly CombatLoadSession _loadSession = new();
private readonly CombatEventBridge _eventBridge = new();
private readonly CombatResourceManager _combatResourceManager = new();
private EntityComponent _entity;
private DRLevel _currentLevel;
private CombatFinishFormUseCase _combatFinishFormUseCase;
private SchedulerState _state = SchedulerState.Idle;
private bool _initialized;
private bool _isFinishAsVictory = true;
public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase;
public bool IsCompleted => _state == SchedulerState.Completed;
public DRLevel CurrentLevel => _currentLevel;
public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase;
public MapEntity CurrentMap => _loadSession.CurrentMap;
public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex;
public int PhaseCount => _phaseLoopRuntime.PhaseCount;
public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat;
public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount;
public int GainedCoin => _combatResourceManager.GainedCoin;
public int GainedGold => _combatResourceManager.GainedGold;
public void OnInit()
{
if (!_initialized)
{
_entity = GameEntry.Entity;
_eventBridge.Bind(
OnShowEntitySuccess,
OnShowEntityFailure,
OnHideEntityComplete,
OnOpenUIFormSuccess,
OnOpenUIFormFailure,
OnCloseUIFormComplete);
_enemyManager.OnInit(this);
_loadSession.OnInit(_entity);
EnsureCombatFinishFormUseCaseBound();
_initialized = true;
}
ResetRuntime();
}
public bool Start(
DRLevel level,
IReadOnlyList<DRLevelPhase> phases,
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId)
{
if (!_initialized || _entity == null)
{
return HandleStartFailure("CombatScheduler start failed. Runtime is not initialized.");
}
if (level == null || phases == null || phases.Count <= 0)
{
return HandleStartFailure("CombatScheduler start failed. Invalid level or phase data.");
}
CleanupAllCombatEntities();
CloseCombatFinishForm();
_enemyManager.EndPhase();
_enemyManager.ResetCombatStats();
ResetRuntime();
_combatResourceManager.Reset();
_isFinishAsVictory = true;
_currentLevel = level;
for (int i = 0; i < phases.Count; i++)
{
DRLevelPhase phase = phases[i];
if (phase != null)
{
_phaseBuffer.Add(phase);
}
}
if (spawnEntriesByPhaseId != null)
{
foreach (var pair in spawnEntriesByPhaseId)
{
_spawnEntriesByPhaseId[pair.Key] = pair.Value;
}
}
_phaseLoopRuntime.SetPhases(_phaseBuffer);
if (_phaseLoopRuntime.PhaseCount <= 0)
{
return HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data.");
}
if (!_loadSession.StartLoading(level, out string loadError))
{
return HandleStartFailure($"CombatScheduler start failed. {loadError}");
}
_state = SchedulerState.WaitingForLoading;
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id,
_phaseLoopRuntime.PhaseCount);
return true;
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
switch (_state)
{
case SchedulerState.WaitingForLoading:
if (!_loadSession.IsReady)
{
return;
}
BeginNextPhase();
return;
case SchedulerState.RunningPhase:
UpdateCurrentPhase(elapseSeconds, realElapseSeconds);
return;
default:
return;
}
}
public void OnDestroy()
{
if (!_initialized)
{
return;
}
CleanupAllCombatEntities();
CloseCombatFinishForm();
_enemyManager.OnDestroy();
ResetRuntime();
_eventBridge.Unbind();
_combatFinishFormUseCase = null;
_entity = null;
_initialized = false;
}
public bool TryEndCombatByPlayer()
{
if (_state != SchedulerState.RunningPhase)
{
return false;
}
if (!_phaseLoopRuntime.TryRequestEndCombat())
{
return false;
}
EnterFinishFlow("Combat ended by player.", true);
return true;
}
private void UpdateCurrentPhase(float elapseSeconds, float realElapseSeconds)
{
DRLevelPhase currentPhase = _phaseLoopRuntime.CurrentPhase;
if (currentPhase == null)
{
EnterFailureFallback("CombatScheduler update failed. Current phase is null.");
return;
}
_phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
_enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted,
_enemyManager.AliveEnemyCount))
{
return;
}
CompleteCurrentPhase();
}
private void CompleteCurrentPhase()
{
_enemyManager.EndPhase();
Log.Info(
"CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.",
_currentLevel != null ? _currentLevel.Id : 0,
_phaseLoopRuntime.CurrentPhase != null ? _phaseLoopRuntime.CurrentPhase.Id : 0,
_phaseLoopRuntime.CurrentPhaseElapsed);
BeginNextPhase();
}
private void BeginNextPhase()
{
if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
{
EnterFinishFlow("Combat ended after loop completion.", true);
return;
}
_spawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries);
_enemyManager.BeginPhase(nextPhase, spawnEntries);
_state = SchedulerState.RunningPhase;
GameEntry.Event.Fire(
this,
CombatProcessEventArgs.Create(_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.PhaseCount));
GameEntry.Event.Fire(
this,
CombatEnemyHpRateChangedEventArgs.Create(
ResolveEnemyHpRateMultiplier(_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.PhaseCount)));
Log.Info(
"CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.",
_currentLevel != null ? _currentLevel.Id : 0,
_phaseLoopRuntime.DisplayPhaseIndex,
nextPhase.EndType,
spawnEntries != null ? spawnEntries.Count : 0);
}
private void EnterFinishFlow(string reason, bool isVictory)
{
int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount;
BackpackInventoryData rewardInventory = _combatResourceManager.GetRewardInventorySnapshot();
// Step 1: stop runtime and clear enemy entities only.
_enemyManager.EndPhase();
_state = SchedulerState.WaitingForFinishReturn;
_isFinishAsVictory = isVictory;
_enemyManager.CleanupTrackedEnemies();
Log.Info(
"CombatScheduler entered finish flow. Level={0}. Reason={1}",
_currentLevel != null ? _currentLevel.Id : 0,
reason);
OpenCombatFinishForm(defeatedEnemyCount, _combatResourceManager.GainedGold, rewardInventory);
}
public void OnEnemyReachedBase(int baseDamage)
{
if (_state != SchedulerState.RunningPhase)
{
return;
}
int resolvedBaseDamage = Mathf.Max(0, baseDamage);
if (resolvedBaseDamage <= 0)
{
return;
}
CombatNodeComponent combatNode = GameEntry.CombatNode;
if (combatNode == null)
{
return;
}
int currentBaseHp = combatNode.ApplyBaseDamage(resolvedBaseDamage);
if (currentBaseHp > 0)
{
return;
}
EnterFinishFlow("Combat ended because base HP reached zero.", false);
}
private void ResetRuntime()
{
_phaseBuffer.Clear();
_spawnEntriesByPhaseId.Clear();
_phaseLoopRuntime.Reset();
_loadSession.Reset();
_combatResourceManager.Reset();
_currentLevel = null;
_isFinishAsVictory = true;
_state = SchedulerState.Idle;
}
private void CleanupAllCombatEntities()
{
_loadSession.Cleanup();
_enemyManager.CleanupTrackedEnemies();
}
private void EnsureCombatFinishFormUseCaseBound()
{
_combatFinishFormUseCase ??= new CombatFinishFormUseCase(this);
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase);
}
private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory)
{
EnsureCombatFinishFormUseCaseBound();
_combatFinishFormUseCase.SetSummary(
defeatedEnemyCount,
gainedGold,
rewardInventory);
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
}
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
{
_combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold);
if (_state != SchedulerState.RunningPhase)
{
return;
}
_combatResourceManager.TryRollOutGameItemDrop(
_phaseLoopRuntime.DisplayPhaseIndex,
ResolveCurrentThemeType());
}
private LevelThemeType ResolveCurrentThemeType()
{
if (_currentLevel != null)
{
return _currentLevel.LevelThemeType;
}
if (GameEntry.CombatNode != null)
{
return GameEntry.CombatNode.CurrentThemeType;
}
return LevelThemeType.None;
}
private void CloseCombatFinishForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
}
public bool OnCombatFinishReturnRequested()
{
if (_state != SchedulerState.WaitingForFinishReturn)
{
return false;
}
// Step 2: clear remaining map/UI resources and close finish form.
_loadSession.Cleanup();
CloseCombatFinishForm();
_state = SchedulerState.Completed;
GameEntry.CombatNode?.OnCombatEndedByScheduler(_isFinishAsVictory);
return true;
}
private bool HandleStartFailure(string errorMessage)
{
Log.Warning("{0}", errorMessage);
_state = SchedulerState.Failed;
_enemyManager.EndPhase();
CleanupAllCombatEntities();
CloseCombatFinishForm();
ResetRuntime();
return false;
}
private void EnterFailureFallback(string errorMessage)
{
if (_state == SchedulerState.Failed || _state == SchedulerState.Completed)
{
return;
}
_state = SchedulerState.Failed;
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0,
errorMessage);
_enemyManager.EndPhase();
CleanupAllCombatEntities();
CloseCombatFinishForm();
GameEntry.CombatNode?.OnCombatEndedByScheduler(false);
}
private static int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount)
{
if (displayPhaseIndex <= 0 || phaseCount <= 0)
{
return 1;
}
int completedLoopCount = Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount);
if (completedLoopCount >= 30)
{
return int.MaxValue;
}
return 1 << completedLoopCount;
}
#region Event Handlers
private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args)
{
var status = _loadSession.HandleShowEntitySuccess(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
EnterFailureFallback(errorMessage);
return;
}
if (status == CombatLoadSession.EventHandleStatus.Succeeded)
{
MapEntity map = _loadSession.CurrentMap;
Log.Info(
"Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.",
_currentLevel != null ? _currentLevel.Id : 0,
map.PathCells.Count,
map.FoundationCells.Count,
map.Spawners.Length,
map.House != null ? map.House.name : "None");
}
}
private void OnShowEntityFailure(ShowEntityFailureEventArgs args)
{
var status = _loadSession.HandleShowEntityFailure(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
EnterFailureFallback(errorMessage);
}
}
private void OnHideEntityComplete(HideEntityCompleteEventArgs args)
{
_loadSession.HandleHideEntityComplete(args);
}
private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args)
{
var status = _loadSession.HandleOpenUIFormSuccess(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
EnterFailureFallback(errorMessage);
}
}
private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args)
{
var status = _loadSession.HandleOpenUIFormFailure(args, out string errorMessage);
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
EnterFailureFallback(errorMessage);
}
}
private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
{
_loadSession.HandleCloseUIFormComplete(args);
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f43fd8050fd4239b4407a4fedcf0b7a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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: