CombatNode 相关逻辑补充

- CombatFinishForm:战斗节点的结算页面(杀死的敌人、获得的金币、获得的道具)
- CombatSelectForm:战斗节点建造防御塔的选择 UI 以及对防御塔进行操作的 UI(只实现了建造逻辑,在 MapEntity 里做射线检测)
- 构建了基本的防御塔三组件(ShooterMuzzleComp/BasicBearingComp/BasicBaseComp)和防御塔主控(DefenseTowerController)以及相应的实体类(DefenseTowerEntity)
This commit is contained in:
SepComet
2026-03-01 23:11:52 +08:00
parent 92cca14503
commit 564817d752
96 changed files with 7294 additions and 121 deletions
@@ -20,7 +20,7 @@ namespace GeometryTD.CustomComponent
}
private EntityComponent _entity;
private CombatInfoFormUseCase _useCase;
private CombatInfoFormUseCase _combatInfoFormUseCase;
private int _loadingMapEntityId;
private int _loadedMapEntityId;
private int? _loadingCombatInfoFormSerialId;
@@ -63,7 +63,7 @@ namespace GeometryTD.CustomComponent
{
return false;
}
return true;
}
@@ -113,7 +113,8 @@ namespace GeometryTD.CustomComponent
return EventHandleStatus.Ignored;
}
if (args.EntityLogicType != typeof(MapEntity) || args.Entity == null || args.Entity.Id != _loadingMapEntityId)
if (args.EntityLogicType != typeof(MapEntity) || args.Entity == null ||
args.Entity.Id != _loadingMapEntityId)
{
return EventHandleStatus.Ignored;
}
@@ -248,10 +249,10 @@ namespace GeometryTD.CustomComponent
private bool TryOpenCombatInfoForm(out string errorMessage)
{
errorMessage = null;
if (_useCase == null)
if (_combatInfoFormUseCase == null)
{
_useCase = new CombatInfoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _useCase);
_combatInfoFormUseCase = new CombatInfoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _combatInfoFormUseCase);
}
int? serialId = GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
@@ -273,4 +274,4 @@ namespace GeometryTD.CustomComponent
_isCombatInfoFormReady = false;
}
}
}
}
@@ -27,11 +27,16 @@ namespace GeometryTD.CustomComponent
private bool _runtimeInitialized;
private int _currentCoin;
private int _currentGold;
public LevelThemeType CurrentThemeType { get; private set; }
public DRLevel CurrentLevel { get; private set; }
public int CurrentCoin => _currentCoin;
public int CurrentGold => _currentGold;
public bool CanEndCombat => _combatScheduler.CanEndCombat;
public int LastDefeatedEnemyCount { get; private set; }
public int LastGainedCoin { get; private set; }
public int LastGainedGold { get; private set; }
public int CurrentLevelPhaseCount
{
@@ -66,6 +71,10 @@ namespace GeometryTD.CustomComponent
CurrentThemeType = themeType;
CurrentLevel = null;
_currentCoin = 0;
_currentGold = 0;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
_levelsById.Clear();
_phasesByLevelId.Clear();
_spawnEntriesByPhaseId.Clear();
@@ -200,13 +209,21 @@ namespace GeometryTD.CustomComponent
CurrentLevel = selectedLevel;
_currentCoin = Mathf.Max(0, selectedLevel.StartCoin);
_currentGold = 0;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
_combatScheduler.Start(selectedLevel, phaseList, _selectedSpawnEntriesByPhaseId);
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
}
public void EndCombat()
{
LastDefeatedEnemyCount = _combatScheduler.DefeatedEnemyCount;
LastGainedCoin = _combatScheduler.GainedCoin;
LastGainedGold = _combatScheduler.GainedGold;
_currentCoin = 0;
_currentGold = 0;
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
}
@@ -229,9 +246,54 @@ namespace GeometryTD.CustomComponent
{
CurrentLevel = null;
_currentCoin = 0;
_currentGold = 0;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
ShutdownCombatRuntime();
}
public void ApplyEnemyDropReward(int coin, int gold)
{
if (coin > 0)
{
_currentCoin += coin;
}
if (gold > 0)
{
_currentGold += gold;
}
}
public bool TryConsumeCoin(int coin)
{
int requiredCoin = Mathf.Max(0, coin);
if (requiredCoin <= 0)
{
return true;
}
if (_currentCoin < requiredCoin)
{
return false;
}
_currentCoin -= requiredCoin;
return true;
}
public void AddCoin(int coin)
{
int gainCoin = Mathf.Max(0, coin);
if (gainCoin <= 0)
{
return;
}
_currentCoin += gainCoin;
}
private void OnDestroy()
{
OnShutdown();
@@ -1,6 +1,9 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
@@ -12,8 +15,9 @@ namespace GeometryTD.CustomComponent
Idle = 0,
WaitingForLoading = 1,
RunningPhase = 2,
Completed = 3,
Failed = 4
WaitingForFinishReturn = 3,
Completed = 4,
Failed = 5
}
private readonly List<DRLevelPhase> _phaseBuffer = new();
@@ -25,8 +29,11 @@ namespace GeometryTD.CustomComponent
private EntityComponent _entity;
private DRLevel _currentLevel;
private CombatFinishFormUseCase _combatFinishFormUseCase;
private SchedulerState _state = SchedulerState.Idle;
private bool _initialized;
private int _gainedCoin;
private int _gainedGold;
public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase;
public bool IsCompleted => _state == SchedulerState.Completed;
@@ -35,6 +42,9 @@ namespace GeometryTD.CustomComponent
public MapEntity CurrentMap => _loadSession.CurrentMap;
public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex;
public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat;
public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount;
public int GainedCoin => _gainedCoin;
public int GainedGold => _gainedGold;
public void OnInit()
{
@@ -50,6 +60,7 @@ namespace GeometryTD.CustomComponent
OnCloseUIFormComplete);
_enemyManager.OnInit(this);
_loadSession.OnInit(_entity);
EnsureCombatFinishFormUseCaseBound();
_initialized = true;
}
@@ -75,9 +86,13 @@ namespace GeometryTD.CustomComponent
return;
}
CleanupCombatEntities();
CleanupAllCombatEntities();
CloseCombatFinishForm();
_enemyManager.EndPhase();
_enemyManager.ResetCombatStats();
ResetRuntime();
_gainedCoin = 0;
_gainedGold = 0;
_currentLevel = level;
for (int i = 0; i < phases.Count; i++)
@@ -113,7 +128,8 @@ namespace GeometryTD.CustomComponent
}
_state = SchedulerState.WaitingForLoading;
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, _phaseLoopRuntime.PhaseCount);
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id,
_phaseLoopRuntime.PhaseCount);
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
@@ -143,17 +159,20 @@ namespace GeometryTD.CustomComponent
return;
}
CleanupCombatEntities();
CleanupAllCombatEntities();
CloseCombatFinishForm();
_enemyManager.OnDestroy();
ResetRuntime();
_eventBridge.Unbind();
_combatFinishFormUseCase = null;
_entity = null;
_initialized = false;
}
public bool TryEndCombatByPlayer()
{
if (_state == SchedulerState.Completed || _state == SchedulerState.Failed || _state == SchedulerState.Idle)
if (_state != SchedulerState.RunningPhase)
{
return false;
}
@@ -163,7 +182,7 @@ namespace GeometryTD.CustomComponent
return false;
}
FinishCombat("Combat ended by player.");
EnterFinishFlow("Combat ended by player.");
return true;
}
@@ -185,7 +204,8 @@ namespace GeometryTD.CustomComponent
_phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
_enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted, _enemyManager.AliveEnemyCount))
if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted,
_enemyManager.AliveEnemyCount))
{
return;
}
@@ -209,7 +229,7 @@ namespace GeometryTD.CustomComponent
{
if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
{
FinishCombat("Combat ended after loop completion.");
EnterFinishFlow("Combat ended after loop completion.");
return;
}
@@ -227,17 +247,21 @@ namespace GeometryTD.CustomComponent
spawnEntries != null ? spawnEntries.Count : 0);
}
private void FinishCombat(string reason)
private void EnterFinishFlow(string reason)
{
_state = SchedulerState.Completed;
CleanupCombatEntities();
int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount;
// Step 1: stop runtime and clear enemy entities only.
_enemyManager.EndPhase();
_state = SchedulerState.WaitingForFinishReturn;
_enemyManager.CleanupTrackedEnemies();
Log.Info(
"CombatScheduler level completed. Level={0}. Reason={1}",
"CombatScheduler entered finish flow. Level={0}. Reason={1}",
_currentLevel != null ? _currentLevel.Id : 0,
reason);
GameEntry.CombatNode.EndCombat();
OpenCombatFinishForm(defeatedEnemyCount, _gainedGold);
}
private void ResetRuntime()
@@ -250,12 +274,57 @@ namespace GeometryTD.CustomComponent
_state = SchedulerState.Idle;
}
private void CleanupCombatEntities()
private void CleanupAllCombatEntities()
{
_loadSession.Cleanup();
_enemyManager.CleanupTrackedEnemies();
}
private void EnsureCombatFinishFormUseCaseBound()
{
_combatFinishFormUseCase ??= new CombatFinishFormUseCase(this);
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase);
}
private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold)
{
EnsureCombatFinishFormUseCaseBound();
_combatFinishFormUseCase.SetSummary(
defeatedEnemyCount,
gainedGold);
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);
}
private void CloseCombatFinishForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
}
public bool OnCombatFinishReturnRequested()
{
if (_state != SchedulerState.WaitingForFinishReturn)
{
return false;
}
// Step 2: clear remaining map/UI resources and close finish form.
_loadSession.Cleanup();
CloseCombatFinishForm();
_state = SchedulerState.Completed;
GameEntry.CombatNode.EndCombat();
return true;
}
#region Event Handlers
private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args)
@@ -287,7 +356,8 @@ namespace GeometryTD.CustomComponent
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_state = SchedulerState.Failed;
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0, errorMessage);
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0,
errorMessage);
}
}
@@ -31,6 +31,7 @@ namespace GeometryTD.CustomComponent
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;
@@ -38,6 +39,7 @@ namespace GeometryTD.CustomComponent
private int _spawnEnemyMaxCount = 5000;
private int _currentEnemyCount;
private int _defeatedEnemyCount;
private int _nextSpawnerIndex;
private int _currentMapEntityId;
private bool _initialized;
@@ -47,6 +49,7 @@ namespace GeometryTD.CustomComponent
private bool _isPhaseRunning;
public int AliveEnemyCount => _currentEnemyCount;
public int DefeatedEnemyCount => _defeatedEnemyCount;
public bool IsPhaseSpawnCompleted { get; private set; } = true;
public bool IsPhaseRunning => _isPhaseRunning;
@@ -61,6 +64,7 @@ namespace GeometryTD.CustomComponent
_entity = GameEntry.Entity;
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
_currentEnemyCount = 0;
_defeatedEnemyCount = 0;
_nextSpawnerIndex = 0;
_currentMapEntityId = 0;
_enemyConfigMissingLogged = false;
@@ -70,6 +74,7 @@ namespace GeometryTD.CustomComponent
_spawnRuntimes.Clear();
_trackedEnemyEntityIds.Clear();
_trackedEnemyIdBuffer.Clear();
_trackedEnemyConfigByEntityId.Clear();
_phaseElapsed = 0f;
_isPhaseRunning = false;
IsPhaseSpawnCompleted = true;
@@ -149,13 +154,20 @@ namespace GeometryTD.CustomComponent
_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)
@@ -171,6 +183,7 @@ namespace GeometryTD.CustomComponent
}
_trackedEnemyEntityIds.Clear();
_trackedEnemyConfigByEntityId.Clear();
_currentEnemyCount = 0;
if (_entity == null)
@@ -352,6 +365,7 @@ namespace GeometryTD.CustomComponent
int enemyEntityId = _entity.GenerateSerialId();
_trackedEnemyEntityIds.Add(enemyEntityId);
_trackedEnemyConfigByEntityId[enemyEntityId] = enemyConfig;
EnemyData enemyData = new EnemyData(
enemyEntityId,
enemyConfig.EntityId,
@@ -497,6 +511,7 @@ namespace GeometryTD.CustomComponent
}
_trackedEnemyEntityIds.Remove(ne.EntityId);
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
}
private void OnHideEntityComplete(object sender, GameEventArgs e)
@@ -508,10 +523,33 @@ namespace GeometryTD.CustomComponent
if (!_trackedEnemyEntityIds.Remove(ne.EntityId))
{
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
return;
}
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId);
if (_combatScheduler != null && _combatScheduler.IsRunning && wasKilled)
{
_defeatedEnemyCount++;
int droppedCoin = 0;
int droppedGold = 0;
if (_trackedEnemyConfigByEntityId.TryGetValue(ne.EntityId, out DREnemy enemyConfig) && enemyConfig != null)
{
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);
}
}
_combatScheduler.OnEnemyDefeatedRewardResolved(droppedCoin, droppedGold);
}
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
}
}
}
}