补全 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,3 @@
fileFormatVersion: 2
guid: c52befd7a6c741b9bcd8f4effd7879b8
timeCreated: 1772440848
@@ -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:
@@ -27,14 +27,13 @@ namespace GeometryTD.CustomComponent
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 int _gainedCoin;
private int _gainedGold;
private bool _isFinishAsVictory = true;
public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase;
@@ -43,10 +42,11 @@ namespace GeometryTD.CustomComponent
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 => _gainedCoin;
public int GainedGold => _gainedGold;
public int GainedCoin => _combatResourceManager.GainedCoin;
public int GainedGold => _combatResourceManager.GainedGold;
public void OnInit()
{
@@ -89,8 +89,7 @@ namespace GeometryTD.CustomComponent
_enemyManager.EndPhase();
_enemyManager.ResetCombatStats();
ResetRuntime();
_gainedCoin = 0;
_gainedGold = 0;
_combatResourceManager.Reset();
_isFinishAsVictory = true;
_currentLevel = level;
@@ -229,6 +228,10 @@ namespace GeometryTD.CustomComponent
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}.",
@@ -241,6 +244,7 @@ namespace GeometryTD.CustomComponent
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();
@@ -253,7 +257,7 @@ namespace GeometryTD.CustomComponent
_currentLevel != null ? _currentLevel.Id : 0,
reason);
OpenCombatFinishForm(defeatedEnemyCount, _gainedGold);
OpenCombatFinishForm(defeatedEnemyCount, _combatResourceManager.GainedGold, rewardInventory);
}
public void OnEnemyReachedBase(int baseDamage)
@@ -290,6 +294,7 @@ namespace GeometryTD.CustomComponent
_spawnEntriesByPhaseId.Clear();
_phaseLoopRuntime.Reset();
_loadSession.Reset();
_combatResourceManager.Reset();
_currentLevel = null;
_isFinishAsVictory = true;
_state = SchedulerState.Idle;
@@ -308,22 +313,43 @@ namespace GeometryTD.CustomComponent
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase);
}
private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold)
private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory)
{
EnsureCombatFinishFormUseCaseBound();
_combatFinishFormUseCase.SetSummary(
defeatedEnemyCount,
gainedGold);
gainedGold,
rewardInventory);
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
}
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
{
int coin = Mathf.Max(0, gainedCoin);
int gold = Mathf.Max(0, gainedGold);
_gainedCoin += coin;
_gainedGold += gold;
GameEntry.CombatNode?.ApplyEnemyDropReward(coin, gold);
_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()
@@ -373,6 +399,22 @@ namespace GeometryTD.CustomComponent
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)
@@ -1,566 +0,0 @@
using System.Collections.Generic;
using GameFramework.DataTable;
using GameFramework.Event;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.Entity.EntityData;
using GeometryTD.Map;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public class EnemyManager
{
private sealed class SpawnEntryRuntime
{
public DRLevelSpawnEntry Entry;
public bool Completed;
public float NextTriggerTime;
public float EndTime;
public int RemainingCount;
}
private const int DefaultEnemyConfigId = 1;
private const float MinStreamInterval = 0.05f;
private const float MinBurstGap = 0.01f;
private readonly List<Spawner> _spawners = new List<Spawner>();
private readonly Dictionary<int, Spawner> _spawnerByOrder = new Dictionary<int, Spawner>();
private readonly List<Vector3> _pathBuffer = new List<Vector3>();
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new List<SpawnEntryRuntime>();
private readonly HashSet<int> _trackedEnemyEntityIds = new HashSet<int>();
private readonly List<int> _trackedEnemyIdBuffer = new List<int>();
private readonly Dictionary<int, DREnemy> _trackedEnemyConfigByEntityId = new Dictionary<int, DREnemy>();
private CombatScheduler _combatScheduler;
private EntityComponent _entity;
private IDataTable<DREnemy> _drEnemy;
private int _spawnEnemyMaxCount = 5000;
private int _currentEnemyCount;
private int _defeatedEnemyCount;
private int _nextSpawnerIndex;
private int _currentMapEntityId;
private bool _initialized;
private bool _enemyConfigMissingLogged;
private float _phaseElapsed;
private bool _isPhaseRunning;
public int AliveEnemyCount => _currentEnemyCount;
public int DefeatedEnemyCount => _defeatedEnemyCount;
public bool IsPhaseSpawnCompleted { get; private set; } = true;
public bool IsPhaseRunning => _isPhaseRunning;
public void OnInit(CombatScheduler combatScheduler)
{
_combatScheduler = combatScheduler;
if (_initialized)
{
return;
}
_entity = GameEntry.Entity;
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
_currentEnemyCount = 0;
_defeatedEnemyCount = 0;
_nextSpawnerIndex = 0;
_currentMapEntityId = 0;
_enemyConfigMissingLogged = false;
_spawners.Clear();
_spawnerByOrder.Clear();
_pathBuffer.Clear();
_spawnRuntimes.Clear();
_trackedEnemyEntityIds.Clear();
_trackedEnemyIdBuffer.Clear();
_trackedEnemyConfigByEntityId.Clear();
_phaseElapsed = 0f;
_isPhaseRunning = false;
IsPhaseSpawnCompleted = true;
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();
_phaseElapsed = 0f;
_isPhaseRunning = true;
IsPhaseSpawnCompleted = false;
RefreshSpawnerCache(true);
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, float realElapseSeconds)
{
if (!_initialized || _combatScheduler == null || !_isPhaseRunning)
{
return;
}
RefreshSpawnerCache(false);
_phaseElapsed += elapseSeconds;
UpdateSpawnRuntimes();
}
public void EndPhase()
{
_isPhaseRunning = false;
_phaseElapsed = 0f;
_spawnRuntimes.Clear();
IsPhaseSpawnCompleted = true;
}
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);
_spawners.Clear();
_spawnerByOrder.Clear();
_pathBuffer.Clear();
_trackedEnemyEntityIds.Clear();
_trackedEnemyIdBuffer.Clear();
_trackedEnemyConfigByEntityId.Clear();
_currentEnemyCount = 0;
_defeatedEnemyCount = 0;
_currentMapEntityId = 0;
_nextSpawnerIndex = 0;
_combatScheduler = null;
_initialized = false;
}
public void ResetCombatStats()
{
_defeatedEnemyCount = 0;
}
public void CleanupTrackedEnemies()
{
if (_trackedEnemyEntityIds.Count <= 0)
{
_currentEnemyCount = 0;
return;
}
_trackedEnemyIdBuffer.Clear();
foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds)
{
_trackedEnemyIdBuffer.Add(trackedEnemyEntityId);
}
_trackedEnemyEntityIds.Clear();
_trackedEnemyConfigByEntityId.Clear();
_currentEnemyCount = 0;
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 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()
{
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);
break;
case EntryType.Burst:
case EntryType.Boss:
ProcessBurstRuntime(runtime);
break;
default:
runtime.Completed = true;
break;
}
if (!runtime.Completed)
{
allCompleted = false;
}
}
IsPhaseSpawnCompleted = allCompleted;
}
private void ProcessStreamRuntime(SpawnEntryRuntime runtime)
{
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)
{
SpawnEnemies(runtime.Entry, countPerWave);
runtime.NextTriggerTime += interval;
}
if (runtime.NextTriggerTime > runtime.EndTime)
{
runtime.Completed = true;
}
}
private void ProcessBurstRuntime(SpawnEntryRuntime runtime)
{
if (_phaseElapsed < runtime.NextTriggerTime)
{
return;
}
if (runtime.RemainingCount <= 0)
{
runtime.Completed = true;
return;
}
float gap = runtime.Entry.Gap;
if (gap <= 0f)
{
SpawnEnemies(runtime.Entry, runtime.RemainingCount);
runtime.RemainingCount = 0;
runtime.Completed = true;
return;
}
gap = Mathf.Max(gap, MinBurstGap);
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0)
{
SpawnEnemies(runtime.Entry, 1);
runtime.RemainingCount--;
runtime.NextTriggerTime += gap;
}
runtime.Completed = runtime.RemainingCount <= 0;
}
private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount)
{
if (spawnCount <= 0)
{
return;
}
Spawner spawner = ResolveSpawner(entry.SpawnPointId);
if (spawner == null)
{
return;
}
MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null;
if (currentMap == null ||
!currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) ||
_pathBuffer.Count <= 0)
{
return;
}
DREnemy enemyConfig = GetEnemyConfig(entry.EnemyId);
if (enemyConfig == null)
{
return;
}
for (int i = 0; i < spawnCount; i++)
{
if (_currentEnemyCount >= _spawnEnemyMaxCount)
{
break;
}
int enemyEntityId = _entity.GenerateSerialId();
_trackedEnemyEntityIds.Add(enemyEntityId);
_trackedEnemyConfigByEntityId[enemyEntityId] = enemyConfig;
EnemyData enemyData = new EnemyData(
enemyEntityId,
enemyConfig.EntityId,
_pathBuffer[0],
enemyConfig.BaseHp,
enemyConfig.Speed,
_pathBuffer);
_entity.ShowEnemy(enemyData);
}
}
private DREnemy GetEnemyConfig(int enemyId)
{
if (_drEnemy == null)
{
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
if (_drEnemy == null)
{
if (!_enemyConfigMissingLogged)
{
Log.Warning("EnemyManagerComponent 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("EnemyManagerComponent found no enemy configs.");
_enemyConfigMissingLogged = true;
}
return null;
}
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;
}
private void RefreshSpawnerCache(bool force)
{
MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null;
if (currentMap == null)
{
_spawners.Clear();
_spawnerByOrder.Clear();
_currentMapEntityId = 0;
_nextSpawnerIndex = 0;
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));
}
private void OnShowEntitySuccess(object sender, GameEventArgs e)
{
if (!(e is ShowEntitySuccessEventArgs ne)) return;
if (ne.EntityLogicType == typeof(EnemyEntity) &&
_trackedEnemyEntityIds.Contains(ne.Entity.Id))
{
_currentEnemyCount++;
}
}
private void OnShowEntityFailure(object sender, GameEventArgs e)
{
if (!(e is ShowEntityFailureEventArgs ne))
{
return;
}
if (ne.EntityLogicType != typeof(EnemyEntity))
{
return;
}
_trackedEnemyEntityIds.Remove(ne.EntityId);
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
}
private void OnHideEntityComplete(object sender, GameEventArgs e)
{
if (!(e is HideEntityCompleteEventArgs ne))
{
return;
}
if (!_trackedEnemyEntityIds.Remove(ne.EntityId))
{
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
return;
}
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId);
bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning;
int baseDamage = 0;
int droppedCoin = 0;
int droppedGold = 0;
if (_trackedEnemyConfigByEntityId.TryGetValue(ne.EntityId, out DREnemy enemyConfig) && enemyConfig != null)
{
baseDamage = Mathf.Max(0, enemyConfig.BaseDamage);
if (wasKilled)
{
droppedCoin = Mathf.Max(0, enemyConfig.DropCoin);
float dropRate = enemyConfig.DropPercent > 1f
? Mathf.Clamp01(enemyConfig.DropPercent * 0.01f)
: Mathf.Clamp01(enemyConfig.DropPercent);
if (enemyConfig.DropGold > 0 && dropRate > 0f && Random.value <= dropRate)
{
droppedGold = Mathf.Max(0, enemyConfig.DropGold);
}
}
}
if (isCombatRunning && wasKilled)
{
_defeatedEnemyCount++;
_combatScheduler.OnEnemyDefeatedRewardResolved(droppedCoin, droppedGold);
}
else if (isCombatRunning && baseDamage > 0)
{
_combatScheduler.OnEnemyReachedBase(baseDamage);
}
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d8e23ddc10c742359bfeb8c6bd765ee2
timeCreated: 1772440818
@@ -0,0 +1,104 @@
using System;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public sealed class EnemyConfigService
{
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("EnemyManagerComponent 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("EnemyManagerComponent 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,3 @@
fileFormatVersion: 2
guid: be15abd89ef8487eabfe09816f202a98
timeCreated: 1772441787
@@ -0,0 +1,75 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemyLifecycleTracker
{
private readonly HashSet<int> _trackedEnemyEntityIds = new();
private readonly Dictionary<int, DREnemy> _trackedEnemyConfigByEntityId = new();
public int AliveEnemyCount { get; private set; }
public void Reset()
{
_trackedEnemyEntityIds.Clear();
_trackedEnemyConfigByEntityId.Clear();
AliveEnemyCount = 0;
}
public void TrackEnemy(int entityId, DREnemy enemyConfig)
{
_trackedEnemyEntityIds.Add(entityId);
_trackedEnemyConfigByEntityId[entityId] = enemyConfig;
}
public bool Contains(int entityId)
{
return _trackedEnemyEntityIds.Contains(entityId);
}
public void HandleShowSuccess(int entityId)
{
if (_trackedEnemyEntityIds.Contains(entityId))
{
AliveEnemyCount++;
}
}
public void HandleShowFailure(int entityId)
{
_trackedEnemyEntityIds.Remove(entityId);
_trackedEnemyConfigByEntityId.Remove(entityId);
}
public bool TryHandleHideComplete(int entityId, out DREnemy enemyConfig)
{
enemyConfig = null;
if (!_trackedEnemyEntityIds.Remove(entityId))
{
_trackedEnemyConfigByEntityId.Remove(entityId);
return false;
}
_trackedEnemyConfigByEntityId.TryGetValue(entityId, out enemyConfig);
_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,11 @@
fileFormatVersion: 2
guid: 0437a4bd33e6a83429ef65cb81a2da0b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,241 @@
using System.Collections.Generic;
using GameFramework.Event;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
using Random = UnityEngine.Random;
namespace GeometryTD.CustomComponent
{
public class EnemyManager
{
private readonly List<int> _trackedEnemyIdBuffer = new();
private readonly EnemySpawnDirector _enemySpawnDirector = new();
private readonly EnemyConfigService _enemyConfigService = new();
private readonly SpawnerResolver _spawnerResolver = 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 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();
_enemyConfigService.Reset();
_spawnerResolver.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();
_spawnerResolver.RefreshCache(_combatScheduler, true);
_enemySpawnDirector.BeginPhase(spawnEntries);
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_initialized || _combatScheduler == null || !_enemySpawnDirector.IsPhaseRunning)
{
return;
}
_spawnerResolver.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);
_spawnerResolver.Reset();
_trackedEnemyIdBuffer.Clear();
_enemyLifecycleTracker.Reset();
_defeatedEnemyCount = 0;
_enemyConfigService.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 (!_spawnerResolver.TryResolveSpawnPath(_combatScheduler, entry.SpawnPointId, out IReadOnlyList<Vector3> pathPoints))
{
return;
}
DREnemy enemyConfig = _enemyConfigService.GetEnemyConfig(entry.EnemyId);
if (enemyConfig == null)
{
return;
}
int scaledBaseHp = _enemyConfigService.ResolveScaledEnemyBaseHp(enemyConfig.BaseHp, _combatScheduler);
for (int i = 0; i < spawnCount; i++)
{
int enemyEntityId = _entity.GenerateSerialId();
_enemyLifecycleTracker.TrackEnemy(enemyEntityId, enemyConfig);
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;
int baseDamage = 0;
int droppedCoin = 0;
int droppedGold = 0;
if (enemyConfig != null)
{
baseDamage = Mathf.Max(0, enemyConfig.BaseDamage);
if (wasKilled)
{
droppedCoin = Mathf.Max(0, enemyConfig.DropCoin);
float dropRate = enemyConfig.DropPercent > 1f
? Mathf.Clamp01(enemyConfig.DropPercent * 0.01f)
: Mathf.Clamp01(enemyConfig.DropPercent);
if (enemyConfig.DropGold > 0 && dropRate > 0f && Random.value <= dropRate)
{
droppedGold = Mathf.Max(0, enemyConfig.DropGold);
}
}
}
if (isCombatRunning && wasKilled)
{
_defeatedEnemyCount++;
_combatScheduler.OnEnemyDefeatedRewardResolved(droppedCoin, droppedGold);
}
else if (isCombatRunning && baseDamage > 0)
{
_combatScheduler.OnEnemyReachedBase(baseDamage);
}
}
}
}
@@ -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,11 @@
fileFormatVersion: 2
guid: 2f7a94ed9eec61c4ea165b6904613ab6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,110 @@
using System.Collections.Generic;
using GeometryTD.Entity;
using GeometryTD.Map;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class SpawnerResolver
{
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e3978ec852f64184e8393143a1c5484d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: