补全 CombatNode 逻辑 + 重构 EnemyManager
- 添加了关卡内的难度系数与掉落
- 难度系数:每循环一轮波次敌人血量翻倍
- 道具掉落:按照掉落概率曲线根据波次计算当前爆率,并从新增的道具池里选择道具
- `CombatResourceManager`:统一维护关卡内资源状态(硬币/金币/道具掉落)
- 重构 `EnemyManager`:
- `EnemyManager`:编排子服务,不承载具体业务细节
- `EnemySpawnDirector`:管理刷怪时序
- `SpawnerResolver`:管理出生点与路径
- `EnemyLifecycleTracker`:追踪敌人生命周期
- `EnemyConfigService`:管理敌人配置与倍率
This commit is contained in:
+104
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be15abd89ef8487eabfe09816f202a98
|
||||
timeCreated: 1772441787
|
||||
+75
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d840df1b53db744cb522b82426f2cd5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+203
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3978ec852f64184e8393143a1c5484d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user