This commit is contained in:
2026-04-30 19:33:16 +08:00
commit fb2252f688
556 changed files with 69721 additions and 0 deletions
@@ -0,0 +1,104 @@
using System;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public sealed class EnemyConfigProvider
{
private const int DefaultEnemyConfigId = 1;
private IDataTable<DREnemy> _drEnemy;
private bool _enemyConfigMissingLogged;
public void Reset()
{
_drEnemy = null;
_enemyConfigMissingLogged = false;
}
public DREnemy GetEnemyConfig(int enemyId)
{
if (_drEnemy == null)
{
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
if (_drEnemy == null)
{
if (!_enemyConfigMissingLogged)
{
Log.Warning("EnemyConfigProvider can not find DREnemy data table.");
_enemyConfigMissingLogged = true;
}
return null;
}
}
if (enemyId > 0)
{
DREnemy targetConfig = _drEnemy.GetDataRow(enemyId);
if (targetConfig != null)
{
return targetConfig;
}
}
DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId);
if (defaultConfig != null)
{
return defaultConfig;
}
DREnemy[] allConfigs = _drEnemy.GetAllDataRows();
if (allConfigs.Length > 0)
{
return allConfigs[0];
}
if (!_enemyConfigMissingLogged)
{
Log.Warning("EnemyConfigProvider found no enemy configs.");
_enemyConfigMissingLogged = true;
}
return null;
}
public int ResolveScaledEnemyBaseHp(int baseHp, CombatScheduler combatScheduler)
{
int resolvedBaseHp = Mathf.Max(1, baseHp);
int completedLoopCount = ResolveCompletedLoopCount(combatScheduler);
if (completedLoopCount <= 0)
{
return resolvedBaseHp;
}
double scaled = resolvedBaseHp * Math.Pow(2d, completedLoopCount);
if (scaled >= int.MaxValue)
{
return int.MaxValue;
}
return Math.Max(1, (int)Math.Round(scaled));
}
private static int ResolveCompletedLoopCount(CombatScheduler combatScheduler)
{
if (combatScheduler == null)
{
return 0;
}
int phaseCount = combatScheduler.PhaseCount;
int displayPhaseIndex = combatScheduler.DisplayPhaseIndex;
if (phaseCount <= 0 || displayPhaseIndex <= 0)
{
return 0;
}
return Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount);
}
}
}
@@ -0,0 +1,91 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemyLifecycleTracker
{
private readonly HashSet<int> _aliveBossEntityIds = new();
private readonly HashSet<int> _trackedEnemyEntityIds = new();
private readonly Dictionary<int, DREnemy> _trackedEnemyConfigByEntityId = new();
private readonly Dictionary<int, bool> _bossFlagsByEntityId = new();
public int AliveEnemyCount { get; private set; }
public bool HasAliveBoss => _aliveBossEntityIds.Count > 0;
public void Reset()
{
_aliveBossEntityIds.Clear();
_bossFlagsByEntityId.Clear();
_trackedEnemyEntityIds.Clear();
_trackedEnemyConfigByEntityId.Clear();
AliveEnemyCount = 0;
}
public void TrackEnemy(int entityId, DREnemy enemyConfig, bool isBoss)
{
_trackedEnemyEntityIds.Add(entityId);
_trackedEnemyConfigByEntityId[entityId] = enemyConfig;
_bossFlagsByEntityId[entityId] = isBoss;
}
public bool Contains(int entityId)
{
return _trackedEnemyEntityIds.Contains(entityId);
}
public void HandleShowSuccess(int entityId)
{
if (_trackedEnemyEntityIds.Contains(entityId))
{
AliveEnemyCount++;
if (_bossFlagsByEntityId.TryGetValue(entityId, out bool isBoss) && isBoss)
{
_aliveBossEntityIds.Add(entityId);
}
}
}
public void HandleShowFailure(int entityId)
{
_aliveBossEntityIds.Remove(entityId);
_bossFlagsByEntityId.Remove(entityId);
_trackedEnemyEntityIds.Remove(entityId);
_trackedEnemyConfigByEntityId.Remove(entityId);
}
public bool TryHandleHideComplete(int entityId, out DREnemy enemyConfig)
{
enemyConfig = null;
if (!_trackedEnemyEntityIds.Remove(entityId))
{
_aliveBossEntityIds.Remove(entityId);
_bossFlagsByEntityId.Remove(entityId);
_trackedEnemyConfigByEntityId.Remove(entityId);
return false;
}
_trackedEnemyConfigByEntityId.TryGetValue(entityId, out enemyConfig);
_aliveBossEntityIds.Remove(entityId);
_bossFlagsByEntityId.Remove(entityId);
_trackedEnemyConfigByEntityId.Remove(entityId);
AliveEnemyCount = Mathf.Max(0, AliveEnemyCount - 1);
return true;
}
public void CopyTrackedEntityIdsTo(List<int> buffer)
{
if (buffer == null)
{
return;
}
buffer.Clear();
foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds)
{
buffer.Add(trackedEnemyEntityId);
}
}
}
}
@@ -0,0 +1,223 @@
using System.Collections.Generic;
using GameFramework.Event;
using GeometryTD.DataTable;
using GeometryTD.Entity;
using GeometryTD.Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public class EnemyManager
{
private readonly List<int> _trackedEnemyIdBuffer = new();
private readonly EnemySpawnDirector _enemySpawnDirector = new();
private readonly EnemyConfigProvider _enemyConfigProvider = new();
private readonly EnemySpawnPathResolver _enemySpawnPathResolver = new();
private readonly EnemyLifecycleTracker _enemyLifecycleTracker = new();
private CombatScheduler _combatScheduler;
private EntityComponent _entity;
private int _defeatedEnemyCount;
private bool _initialized;
public int AliveEnemyCount => _enemyLifecycleTracker.AliveEnemyCount;
public int DefeatedEnemyCount => _defeatedEnemyCount;
public bool HasAliveBoss => _enemyLifecycleTracker.HasAliveBoss;
public bool IsPhaseSpawnCompleted => _enemySpawnDirector.IsPhaseSpawnCompleted;
public bool IsPhaseRunning => _enemySpawnDirector.IsPhaseRunning;
public void OnInit(CombatScheduler combatScheduler)
{
_combatScheduler = combatScheduler;
if (_initialized)
{
return;
}
_entity = GameEntry.Entity;
_defeatedEnemyCount = 0;
_enemySpawnDirector.Reset();
_enemyConfigProvider.Reset();
_enemySpawnPathResolver.Reset();
_trackedEnemyIdBuffer.Clear();
_enemyLifecycleTracker.Reset();
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_initialized = true;
}
public void BeginPhase(DRLevelPhase phase, IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
{
if (!_initialized || _combatScheduler == null)
{
return;
}
_ = phase;
EndPhase();
_enemySpawnPathResolver.RefreshCache(_combatScheduler, true);
_enemySpawnDirector.BeginPhase(spawnEntries);
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_initialized || _combatScheduler == null || !_enemySpawnDirector.IsPhaseRunning)
{
return;
}
_enemySpawnPathResolver.RefreshCache(_combatScheduler, false);
_enemySpawnDirector.OnUpdate(elapseSeconds, SpawnEnemies);
}
public void EndPhase()
{
_enemySpawnDirector.EndPhase();
}
public void OnDestroy()
{
if (!_initialized)
{
_combatScheduler = null;
return;
}
CleanupTrackedEnemies();
EndPhase();
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_enemySpawnPathResolver.Reset();
_trackedEnemyIdBuffer.Clear();
_enemyLifecycleTracker.Reset();
_defeatedEnemyCount = 0;
_enemyConfigProvider.Reset();
_combatScheduler = null;
_initialized = false;
}
public void ResetCombatStats()
{
_defeatedEnemyCount = 0;
}
public void CleanupTrackedEnemies()
{
_enemyLifecycleTracker.CopyTrackedEntityIdsTo(_trackedEnemyIdBuffer);
if (_trackedEnemyIdBuffer.Count <= 0)
{
return;
}
_enemyLifecycleTracker.Reset();
if (_entity == null)
{
return;
}
for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++)
{
int trackedEnemyEntityId = _trackedEnemyIdBuffer[i];
if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId))
{
_entity.HideEntity(trackedEnemyEntityId);
}
}
}
private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount)
{
if (spawnCount <= 0)
{
return;
}
if (!_enemySpawnPathResolver.TryResolveSpawnPath(_combatScheduler, entry.SpawnPointId, out IReadOnlyList<Vector3> pathPoints))
{
return;
}
DREnemy enemyConfig = _enemyConfigProvider.GetEnemyConfig(entry.EnemyId);
if (enemyConfig == null)
{
return;
}
int scaledBaseHp = _enemyConfigProvider.ResolveScaledEnemyBaseHp(enemyConfig.BaseHp, _combatScheduler);
bool isBoss = entry.EntryType == Definition.EntryType.Boss;
for (int i = 0; i < spawnCount; i++)
{
int enemyEntityId = _entity.GenerateSerialId();
_enemyLifecycleTracker.TrackEnemy(enemyEntityId, enemyConfig, isBoss);
EnemyData enemyData = new EnemyData(
enemyEntityId,
enemyConfig.EntityId,
pathPoints[0],
scaledBaseHp,
enemyConfig.Speed,
pathPoints);
_entity.ShowEnemy(enemyData);
}
}
private void OnShowEntitySuccess(object sender, GameEventArgs e)
{
if (!(e is ShowEntitySuccessEventArgs ne)) return;
if (ne.EntityLogicType == typeof(EnemyEntity) &&
_enemyLifecycleTracker.Contains(ne.Entity.Id))
{
_enemyLifecycleTracker.HandleShowSuccess(ne.Entity.Id);
}
}
private void OnShowEntityFailure(object sender, GameEventArgs e)
{
if (!(e is ShowEntityFailureEventArgs ne))
{
return;
}
if (ne.EntityLogicType != typeof(EnemyEntity))
{
return;
}
_enemyLifecycleTracker.HandleShowFailure(ne.EntityId);
}
private void OnHideEntityComplete(object sender, GameEventArgs e)
{
if (!(e is HideEntityCompleteEventArgs ne))
{
return;
}
if (!_enemyLifecycleTracker.TryHandleHideComplete(ne.EntityId, out DREnemy enemyConfig))
{
return;
}
bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId);
bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning;
if (isCombatRunning && wasKilled && enemyConfig != null)
{
_defeatedEnemyCount++;
_combatScheduler.OnEnemyDefeated(enemyConfig);
}
else if (isCombatRunning && !wasKilled && enemyConfig != null)
{
_combatScheduler.OnEnemyReachedBase(enemyConfig);
}
}
}
}
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemySpawnDirector
{
private sealed class SpawnEntryRuntime
{
public DRLevelSpawnEntry Entry;
public bool Completed;
public float NextTriggerTime;
public float EndTime;
public int RemainingCount;
}
private const float MinStreamInterval = 0.05f;
private const float MinBurstGap = 0.01f;
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new();
private float _phaseElapsed;
public bool IsPhaseSpawnCompleted { get; private set; } = true;
public bool IsPhaseRunning { get; private set; }
public void Reset()
{
EndPhase();
}
public void BeginPhase(IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
{
EndPhase();
_phaseElapsed = 0f;
IsPhaseRunning = true;
IsPhaseSpawnCompleted = false;
if (spawnEntries != null)
{
for (int i = 0; i < spawnEntries.Count; i++)
{
SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]);
if (runtime != null)
{
_spawnRuntimes.Add(runtime);
}
}
}
IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0;
}
public void OnUpdate(float elapseSeconds, Action<DRLevelSpawnEntry, int> spawnAction)
{
if (!IsPhaseRunning || spawnAction == null)
{
return;
}
_phaseElapsed += elapseSeconds;
UpdateSpawnRuntimes(spawnAction);
}
public void EndPhase()
{
IsPhaseRunning = false;
_phaseElapsed = 0f;
_spawnRuntimes.Clear();
IsPhaseSpawnCompleted = true;
}
private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry)
{
if (entry == null || entry.EntryType == EntryType.None)
{
return null;
}
SpawnEntryRuntime runtime = new SpawnEntryRuntime
{
Entry = entry,
Completed = false,
NextTriggerTime = Mathf.Max(0f, entry.StartTime),
EndTime = Mathf.Max(0f, entry.StartTime),
RemainingCount = Mathf.Max(0, entry.Count)
};
switch (entry.EntryType)
{
case EntryType.Stream:
{
float duration = Mathf.Max(0f, entry.Duration);
runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime;
runtime.Completed = entry.Count <= 0;
return runtime;
}
case EntryType.Burst:
case EntryType.Boss:
runtime.Completed = runtime.RemainingCount <= 0;
return runtime;
default:
return null;
}
}
private void UpdateSpawnRuntimes(Action<DRLevelSpawnEntry, int> spawnAction)
{
bool allCompleted = true;
for (int i = 0; i < _spawnRuntimes.Count; i++)
{
SpawnEntryRuntime runtime = _spawnRuntimes[i];
if (runtime.Completed)
{
continue;
}
switch (runtime.Entry.EntryType)
{
case EntryType.Stream:
ProcessStreamRuntime(runtime, spawnAction);
break;
case EntryType.Burst:
case EntryType.Boss:
ProcessBurstRuntime(runtime, spawnAction);
break;
default:
runtime.Completed = true;
break;
}
if (!runtime.Completed)
{
allCompleted = false;
}
}
IsPhaseSpawnCompleted = allCompleted;
}
private void ProcessStreamRuntime(SpawnEntryRuntime runtime, Action<DRLevelSpawnEntry, int> spawnAction)
{
if (_phaseElapsed < runtime.NextTriggerTime)
{
return;
}
int countPerWave = Mathf.Max(0, runtime.Entry.Count);
if (countPerWave <= 0)
{
runtime.Completed = true;
return;
}
float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval;
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime)
{
spawnAction(runtime.Entry, countPerWave);
runtime.NextTriggerTime += interval;
}
if (runtime.NextTriggerTime > runtime.EndTime)
{
runtime.Completed = true;
}
}
private void ProcessBurstRuntime(SpawnEntryRuntime runtime, Action<DRLevelSpawnEntry, int> spawnAction)
{
if (_phaseElapsed < runtime.NextTriggerTime)
{
return;
}
if (runtime.RemainingCount <= 0)
{
runtime.Completed = true;
return;
}
float gap = runtime.Entry.Gap;
if (gap <= 0f)
{
spawnAction(runtime.Entry, runtime.RemainingCount);
runtime.RemainingCount = 0;
runtime.Completed = true;
return;
}
gap = Mathf.Max(gap, MinBurstGap);
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0)
{
spawnAction(runtime.Entry, 1);
runtime.RemainingCount--;
runtime.NextTriggerTime += gap;
}
runtime.Completed = runtime.RemainingCount <= 0;
}
}
}
@@ -0,0 +1,110 @@
using System.Collections.Generic;
using GeometryTD.Entity;
using GeometryTD.Map;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemySpawnPathResolver
{
private readonly List<Spawner> _spawners = new();
private readonly Dictionary<int, Spawner> _spawnerByOrder = new();
private readonly List<Vector3> _pathBuffer = new();
private int _nextSpawnerIndex;
private int _currentMapEntityId;
public void Reset()
{
_spawners.Clear();
_spawnerByOrder.Clear();
_pathBuffer.Clear();
_nextSpawnerIndex = 0;
_currentMapEntityId = 0;
}
public void RefreshCache(CombatScheduler combatScheduler, bool force)
{
MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null;
if (currentMap == null)
{
Reset();
return;
}
if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0)
{
return;
}
_spawners.Clear();
_spawnerByOrder.Clear();
_nextSpawnerIndex = 0;
_currentMapEntityId = currentMap.Id;
Spawner[] mapSpawners = currentMap.Spawners;
for (int i = 0; i < mapSpawners.Length; i++)
{
Spawner spawner = mapSpawners[i];
if (spawner == null)
{
continue;
}
if (!currentMap.TryGetDefaultPathCells(spawner, out _))
{
continue;
}
_spawners.Add(spawner);
if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder))
{
_spawnerByOrder[spawner.SpawnOrder] = spawner;
}
}
_spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder));
}
public bool TryResolveSpawnPath(CombatScheduler combatScheduler, int spawnPointId, out IReadOnlyList<Vector3> pathPoints)
{
pathPoints = null;
MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null;
if (currentMap == null)
{
return false;
}
Spawner spawner = ResolveSpawner(spawnPointId);
if (spawner == null)
{
return false;
}
if (!currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) || _pathBuffer.Count <= 0)
{
return false;
}
pathPoints = _pathBuffer;
return true;
}
private Spawner ResolveSpawner(int spawnPointId)
{
if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner))
{
return mappedSpawner;
}
if (_spawners.Count <= 0)
{
return null;
}
Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count];
_nextSpawnerIndex++;
return fallbackSpawner;
}
}
}