补全 CombatNode 逻辑 + 重构 EnemyManager
- 添加了关卡内的难度系数与掉落
- 难度系数:每循环一轮波次敌人血量翻倍
- 道具掉落:按照掉落概率曲线根据波次计算当前爆率,并从新增的道具池里选择道具
- `CombatResourceManager`:统一维护关卡内资源状态(硬币/金币/道具掉落)
- 重构 `EnemyManager`:
- `EnemyManager`:编排子服务,不承载具体业务细节
- `EnemySpawnDirector`:管理刷怪时序
- `SpawnerResolver`:管理出生点与路径
- `EnemyLifecycleTracker`:追踪敌人生命周期
- `EnemyConfigService`:管理敌人配置与倍率
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c52befd7a6c741b9bcd8f4effd7879b8
|
||||
timeCreated: 1772440848
|
||||
+398
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 650463d0d65211e4e969ea0d469516d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+56
-14
@@ -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
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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:
|
||||
@@ -28,10 +28,15 @@ namespace GeometryTD.DataTable
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取底座组件攻击速度数组
|
||||
/// 获取底座组件攻击速度数组(秒/次)
|
||||
/// </summary>
|
||||
public float[] AttackSpeed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取底座组件每级提升攻击速度(值为负数)
|
||||
/// </summary>
|
||||
public float AttackSpeedPerLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取攻击属性
|
||||
/// </summary>
|
||||
@@ -62,6 +67,7 @@ namespace GeometryTD.DataTable
|
||||
EntityId = int.Parse(columnStrings[index++]);
|
||||
Name = columnStrings[index++];
|
||||
AttackSpeed = GenerateAttackSpeed(columnStrings[index++]);
|
||||
AttackSpeedPerLevel = float.Parse(columnStrings[index++]);
|
||||
AttackPropertyType = EnumUtility<AttackPropertyType>.Get(columnStrings[index++]);
|
||||
Constraint = columnStrings[index++];
|
||||
PossibleTag = GeneratePossibleTag(columnStrings[index++]);
|
||||
|
||||
@@ -32,11 +32,21 @@ namespace GeometryTD.DataTable
|
||||
/// </summary>
|
||||
public float[] RotateSpeed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取轴承组件每级提升旋转速度
|
||||
/// </summary>
|
||||
public float RotateSpeedPerLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取攻击范围
|
||||
/// </summary>
|
||||
public float[] AttackRange { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取每级提升攻击范围
|
||||
/// </summary>
|
||||
public float AttackRangePerLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取属性约束
|
||||
/// </summary>
|
||||
@@ -62,7 +72,9 @@ namespace GeometryTD.DataTable
|
||||
EntityId = int.Parse(columnStrings[index++]);
|
||||
Name = columnStrings[index++];
|
||||
RotateSpeed = GenerateFloatArray(columnStrings[index++]);
|
||||
RotateSpeedPerLevel = float.Parse(columnStrings[index++]);
|
||||
AttackRange = GenerateFloatArray(columnStrings[index++]);
|
||||
AttackRangePerLevel = float.Parse(columnStrings[index++]);
|
||||
Constraint = columnStrings[index++];
|
||||
PossibleTag = GeneratePossibleTag(columnStrings[index++]);
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ namespace GeometryTD.DataTable
|
||||
/// </summary>
|
||||
public int[] AttackDamage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取枪口组件每级提升攻击伤害值
|
||||
/// </summary>
|
||||
public int AttackDamagePerLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取攻击伤害浮动
|
||||
/// </summary>
|
||||
@@ -67,6 +72,7 @@ namespace GeometryTD.DataTable
|
||||
EntityId = int.Parse(columnStrings[index++]);
|
||||
Name = columnStrings[index++];
|
||||
AttackDamage = GenerateAttackDamage(columnStrings[index++]);
|
||||
AttackDamagePerLevel = int.Parse(columnStrings[index++]);
|
||||
DamageRandomRate = float.Parse(columnStrings[index++]);
|
||||
AttackMethodType = EnumUtility<AttackMethodType>.Get(columnStrings[index++]);
|
||||
Constraint = columnStrings[index++];
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 局外道具掉落池配置表。
|
||||
/// </summary>
|
||||
public class DROutGameDropPool : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
public override int Id => m_Id;
|
||||
|
||||
public LevelThemeType LevelThemeType { get; private set; }
|
||||
public RarityType Rarity { get; private set; }
|
||||
public string ItemType { get; private set; }
|
||||
public int ItemId { get; private set; }
|
||||
public int Weight { get; private set; }
|
||||
public int MinPhase { get; private set; }
|
||||
public int MaxPhase { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
LevelThemeType = EnumUtility<LevelThemeType>.Get(columnStrings[index++]);
|
||||
Rarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
|
||||
ItemType = columnStrings[index++];
|
||||
ItemId = ParseIntOrDefault(columnStrings[index++], 0);
|
||||
Weight = ParseIntOrDefault(columnStrings[index++], 1);
|
||||
MinPhase = ParseIntOrDefault(columnStrings[index++], 1);
|
||||
MaxPhase = ParseIntOrDefault(columnStrings[index++], int.MaxValue);
|
||||
|
||||
if (Weight <= 0)
|
||||
{
|
||||
Weight = 1;
|
||||
}
|
||||
|
||||
if (MinPhase <= 0)
|
||||
{
|
||||
MinPhase = 1;
|
||||
}
|
||||
|
||||
if (MaxPhase < MinPhase)
|
||||
{
|
||||
MaxPhase = MinPhase;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int ParseIntOrDefault(string raw, int fallbackValue)
|
||||
{
|
||||
if (int.TryParse(raw, out int parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a06af49028f1aa47acede2e0651f2c3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
public class CombatEnemyHpRateChangedEventArgs : GameEventArgs
|
||||
{
|
||||
public static int EventId => typeof(CombatEnemyHpRateChangedEventArgs).GetHashCode();
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public int EnemyHpRateMultiplier { get; private set; }
|
||||
|
||||
public CombatEnemyHpRateChangedEventArgs()
|
||||
{
|
||||
EnemyHpRateMultiplier = 1;
|
||||
}
|
||||
|
||||
public static CombatEnemyHpRateChangedEventArgs Create(int enemyHpRateMultiplier)
|
||||
{
|
||||
var args = ReferencePool.Acquire<CombatEnemyHpRateChangedEventArgs>();
|
||||
args.EnemyHpRateMultiplier = enemyHpRateMultiplier > 0 ? enemyHpRateMultiplier : 1;
|
||||
return args;
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
EnemyHpRateMultiplier = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c8ba0b112fc25848bf0fbc4b5919a67
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -31,6 +31,7 @@ namespace GeometryTD.Procedure
|
||||
"ShopPrice",
|
||||
"Sound",
|
||||
"Tag",
|
||||
"OutGameDropPool",
|
||||
"UIForm",
|
||||
"UISound",
|
||||
};
|
||||
@@ -231,4 +232,4 @@ namespace GeometryTD.Procedure
|
||||
ne.DictionaryAssetName, ne.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace GeometryTD.UI
|
||||
public string LevelPhaseText;
|
||||
public string CoinText;
|
||||
public string BaseHpText;
|
||||
public string EnemyHpRateText;
|
||||
public bool CanPause;
|
||||
public bool CanEnd;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace GeometryTD.UI
|
||||
GameEntry.Event.Subscribe(CombatProcessEventArgs.EventId, OnCombatProcessChanged);
|
||||
GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged);
|
||||
GameEntry.Event.Subscribe(CombatBaseHpChangedEventArgs.EventId, OnCombatBaseHpChanged);
|
||||
GameEntry.Event.Subscribe(CombatEnemyHpRateChangedEventArgs.EventId, OnCombatEnemyHpRateChanged);
|
||||
}
|
||||
|
||||
protected override void UnsubscribeCustomEvents()
|
||||
@@ -32,6 +33,7 @@ namespace GeometryTD.UI
|
||||
GameEntry.Event.Unsubscribe(CombatProcessEventArgs.EventId, OnCombatProcessChanged);
|
||||
GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged);
|
||||
GameEntry.Event.Unsubscribe(CombatBaseHpChangedEventArgs.EventId, OnCombatBaseHpChanged);
|
||||
GameEntry.Event.Unsubscribe(CombatEnemyHpRateChangedEventArgs.EventId, OnCombatEnemyHpRateChanged);
|
||||
}
|
||||
|
||||
public int? OpenUI(CombatInfoFormRawData rawData)
|
||||
@@ -92,6 +94,7 @@ namespace GeometryTD.UI
|
||||
LevelPhaseText = BuildPhaseText(rawData.CurrentPhaseIndex, rawData.TotalPhaseCount),
|
||||
CoinText = BuildCoinText(rawData.Coin),
|
||||
BaseHpText = BuildBaseHpText(rawData.BaseHp, rawData.BaseHpMax),
|
||||
EnemyHpRateText = BuildEnemyHpRateText(rawData.EnemyHpRateMultiplier),
|
||||
CanPause = rawData.CanPause,
|
||||
CanEnd = rawData.CanEnd
|
||||
};
|
||||
@@ -140,6 +143,12 @@ namespace GeometryTD.UI
|
||||
return $"\u57FA\u5730\uFF1A{percent}%";
|
||||
}
|
||||
|
||||
private static string BuildEnemyHpRateText(int enemyHpRateMultiplier)
|
||||
{
|
||||
int resolvedMultiplier = enemyHpRateMultiplier > 0 ? enemyHpRateMultiplier : 1;
|
||||
return $"\u96BE\u5EA6\uFF1A{resolvedMultiplier}x";
|
||||
}
|
||||
|
||||
private void OnCombatPauseButtonClicked(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(sender is CombatInfoForm) || !(e is CombatPauseEventArgs))
|
||||
@@ -197,6 +206,16 @@ namespace GeometryTD.UI
|
||||
RefreshFromUseCase();
|
||||
}
|
||||
|
||||
private void OnCombatEnemyHpRateChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombatEnemyHpRateChangedEventArgs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshFromUseCase();
|
||||
}
|
||||
|
||||
private void RefreshFromUseCase()
|
||||
{
|
||||
if (_useCase == null)
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace GeometryTD.UI
|
||||
public int Coin;
|
||||
public int BaseHp;
|
||||
public int BaseHpMax;
|
||||
public int EnemyHpRateMultiplier;
|
||||
public bool CanPause;
|
||||
public bool CanEnd;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace GeometryTD.UI
|
||||
LevelThemeType themeType = level != null ? level.LevelThemeType : GameEntry.CombatNode.CurrentThemeType;
|
||||
int levelId = level != null ? level.Id : 0;
|
||||
int baseHpMax = level != null ? level.BaseHp : 0;
|
||||
int enemyHpRateMultiplier = ResolveEnemyHpRateMultiplier(
|
||||
GameEntry.CombatNode.CurrentPhaseIndex,
|
||||
GameEntry.CombatNode.CurrentLevelPhaseCount);
|
||||
|
||||
return new CombatInfoFormRawData
|
||||
{
|
||||
@@ -45,9 +48,26 @@ namespace GeometryTD.UI
|
||||
Coin = GameEntry.CombatNode.CurrentCoin,
|
||||
BaseHp = GameEntry.CombatNode.CurrentBaseHp,
|
||||
BaseHpMax = baseHpMax,
|
||||
EnemyHpRateMultiplier = enemyHpRateMultiplier,
|
||||
CanPause = true,
|
||||
CanEnd = GameEntry.CombatNode.CanEndCombat
|
||||
};
|
||||
}
|
||||
|
||||
private static int ResolveEnemyHpRateMultiplier(int currentPhaseIndex, int totalPhaseCount)
|
||||
{
|
||||
if (currentPhaseIndex <= 0 || totalPhaseCount <= 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int completedLoopCount = UnityEngine.Mathf.Max(0, (currentPhaseIndex - 1) / totalPhaseCount);
|
||||
if (completedLoopCount >= 30)
|
||||
{
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
return 1 << completedLoopCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace GeometryTD.UI
|
||||
|
||||
[SerializeField] private TMP_Text _baseHpText;
|
||||
|
||||
[SerializeField] private TMP_Text _enemyHpRateText;
|
||||
|
||||
[SerializeField] private CommonButton _pauseButton;
|
||||
|
||||
[SerializeField] private CommonButton _endButton;
|
||||
@@ -44,6 +46,11 @@ namespace GeometryTD.UI
|
||||
{
|
||||
_baseHpText.text = context?.BaseHpText ?? string.Empty;
|
||||
}
|
||||
|
||||
if (_enemyHpRateText != null)
|
||||
{
|
||||
_enemyHpRateText.text = context?.EnemyHpRateText ?? string.Empty;
|
||||
}
|
||||
|
||||
if (_pauseButton != null)
|
||||
{
|
||||
@@ -112,6 +119,11 @@ namespace GeometryTD.UI
|
||||
{
|
||||
_baseHpText.text = string.Empty;
|
||||
}
|
||||
|
||||
if (_enemyHpRateText != null)
|
||||
{
|
||||
_enemyHpRateText.text = string.Empty;
|
||||
}
|
||||
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
@@ -183,40 +183,25 @@ namespace GeometryTD.UI
|
||||
|
||||
private void RefreshConfirmText(string confirmText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(confirmText))
|
||||
foreach (var text in _confirmTexts)
|
||||
{
|
||||
confirmText = GameEntry.Localization.GetString("Dialog.ConfirmButton");
|
||||
}
|
||||
|
||||
for (int i = 0; i < _confirmTexts.Length; i++)
|
||||
{
|
||||
_confirmTexts[i].text = confirmText;
|
||||
text.text = confirmText;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCancelText(string cancelText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cancelText))
|
||||
foreach (var text in _cancelTexts)
|
||||
{
|
||||
cancelText = GameEntry.Localization.GetString("Dialog.CancelButton");
|
||||
}
|
||||
|
||||
for (int i = 0; i < _cancelTexts.Length; i++)
|
||||
{
|
||||
_cancelTexts[i].text = cancelText;
|
||||
text.text = cancelText;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshOtherText(string otherText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(otherText))
|
||||
foreach (var text in _otherTexts)
|
||||
{
|
||||
otherText = GameEntry.Localization.GetString("Dialog.OtherButton");
|
||||
}
|
||||
|
||||
for (int i = 0; i < _otherTexts.Length; i++)
|
||||
{
|
||||
_otherTexts[i].text = otherText;
|
||||
text.text = otherText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user