init
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using GameFramework.Event;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatEventBridge
|
||||
{
|
||||
private Action<ShowEntitySuccessEventArgs> _onShowEntitySuccess;
|
||||
private Action<ShowEntityFailureEventArgs> _onShowEntityFailure;
|
||||
private Action<HideEntityCompleteEventArgs> _onHideEntityComplete;
|
||||
private Action<OpenUIFormSuccessEventArgs> _onOpenUIFormSuccess;
|
||||
private Action<OpenUIFormFailureEventArgs> _onOpenUIFormFailure;
|
||||
private Action<CloseUIFormCompleteEventArgs> _onCloseUIFormComplete;
|
||||
|
||||
private bool _isEntityEventSubscribed;
|
||||
private bool _isUIEventSubscribed;
|
||||
|
||||
public void Bind(
|
||||
Action<ShowEntitySuccessEventArgs> onShowEntitySuccess,
|
||||
Action<ShowEntityFailureEventArgs> onShowEntityFailure,
|
||||
Action<HideEntityCompleteEventArgs> onHideEntityComplete,
|
||||
Action<OpenUIFormSuccessEventArgs> onOpenUIFormSuccess,
|
||||
Action<OpenUIFormFailureEventArgs> onOpenUIFormFailure,
|
||||
Action<CloseUIFormCompleteEventArgs> onCloseUIFormComplete)
|
||||
{
|
||||
_onShowEntitySuccess = onShowEntitySuccess;
|
||||
_onShowEntityFailure = onShowEntityFailure;
|
||||
_onHideEntityComplete = onHideEntityComplete;
|
||||
_onOpenUIFormSuccess = onOpenUIFormSuccess;
|
||||
_onOpenUIFormFailure = onOpenUIFormFailure;
|
||||
_onCloseUIFormComplete = onCloseUIFormComplete;
|
||||
|
||||
EnsureEntityEventSubscribed();
|
||||
EnsureUIEventSubscribed();
|
||||
}
|
||||
|
||||
public void Unbind()
|
||||
{
|
||||
UnsubscribeEntityEvents();
|
||||
UnsubscribeUIEvents();
|
||||
|
||||
_onShowEntitySuccess = null;
|
||||
_onShowEntityFailure = null;
|
||||
_onHideEntityComplete = null;
|
||||
_onOpenUIFormSuccess = null;
|
||||
_onOpenUIFormFailure = null;
|
||||
_onCloseUIFormComplete = null;
|
||||
}
|
||||
|
||||
private void EnsureEntityEventSubscribed()
|
||||
{
|
||||
if (_isEntityEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_isEntityEventSubscribed = true;
|
||||
}
|
||||
|
||||
private void EnsureUIEventSubscribed()
|
||||
{
|
||||
if (_isUIEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnOpenUIFormSuccess);
|
||||
GameEntry.Event.Subscribe(OpenUIFormFailureEventArgs.EventId, OnOpenUIFormFailure);
|
||||
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnCloseUIFormComplete);
|
||||
_isUIEventSubscribed = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeEntityEvents()
|
||||
{
|
||||
if (!_isEntityEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_isEntityEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void UnsubscribeUIEvents()
|
||||
{
|
||||
if (!_isUIEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnOpenUIFormSuccess);
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormFailureEventArgs.EventId, OnOpenUIFormFailure);
|
||||
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnCloseUIFormComplete);
|
||||
_isUIEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is ShowEntitySuccessEventArgs args)
|
||||
{
|
||||
_onShowEntitySuccess?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is ShowEntityFailureEventArgs args)
|
||||
{
|
||||
_onShowEntityFailure?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is HideEntityCompleteEventArgs args)
|
||||
{
|
||||
_onHideEntityComplete?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenUIFormSuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is OpenUIFormSuccessEventArgs args)
|
||||
{
|
||||
_onOpenUIFormSuccess?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenUIFormFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is OpenUIFormFailureEventArgs args)
|
||||
{
|
||||
_onOpenUIFormFailure?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseUIFormComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is CloseUIFormCompleteEventArgs args)
|
||||
{
|
||||
_onCloseUIFormComplete?.Invoke(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
using GameFramework.Resource;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatLoadSession
|
||||
{
|
||||
internal enum EventHandleStatus : byte
|
||||
{
|
||||
Ignored = 0,
|
||||
Succeeded = 1,
|
||||
Failed = 2
|
||||
}
|
||||
|
||||
private EntityComponent _entity;
|
||||
private CombatInfoFormUseCase _combatInfoFormUseCase;
|
||||
private int _loadingMapEntityId;
|
||||
private int _loadedMapEntityId;
|
||||
private int? _loadingCombatInfoFormSerialId;
|
||||
private bool _isCombatInfoFormReady;
|
||||
private MapEntity _currentMap;
|
||||
|
||||
public MapEntity CurrentMap => _currentMap;
|
||||
public bool IsReady => _currentMap != null && _isCombatInfoFormReady;
|
||||
|
||||
public void OnInit(EntityComponent entityComponent)
|
||||
{
|
||||
_entity = entityComponent;
|
||||
Reset();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_loadingMapEntityId = 0;
|
||||
_loadedMapEntityId = 0;
|
||||
_loadingCombatInfoFormSerialId = null;
|
||||
_isCombatInfoFormReady = false;
|
||||
_currentMap = null;
|
||||
}
|
||||
|
||||
public bool StartLoading(DRLevel level, MapEntityLoadContext mapLoadContext, ICombatSchedulerPort schedulerPort, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (_entity == null)
|
||||
{
|
||||
errorMessage = "CombatLoadSession start failed. Entity component is null.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryShowMap(level, mapLoadContext, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryOpenCombatInfoForm(schedulerPort, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RefreshCombatInfoForm(DRLevel currentLevel)
|
||||
{
|
||||
if (currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
if (_entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_loadingMapEntityId != 0 && _entity.IsLoadingEntity(_loadingMapEntityId))
|
||||
{
|
||||
_entity.HideEntity(_loadingMapEntityId);
|
||||
}
|
||||
|
||||
if (_currentMap != null)
|
||||
{
|
||||
_entity.HideEntity(_currentMap);
|
||||
}
|
||||
else if (_loadedMapEntityId != 0)
|
||||
{
|
||||
UnityGameFramework.Runtime.Entity loadedMapEntity = _entity.GetEntity(_loadedMapEntityId);
|
||||
if (loadedMapEntity != null)
|
||||
{
|
||||
_entity.HideEntity(loadedMapEntity);
|
||||
}
|
||||
}
|
||||
|
||||
CloseCombatInfoForm();
|
||||
}
|
||||
|
||||
public EventHandleStatus HandleShowEntitySuccess(ShowEntitySuccessEventArgs args, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (args == null)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
if (args.EntityLogicType != typeof(MapEntity) || args.Entity == null ||
|
||||
args.Entity.Id != _loadingMapEntityId)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
_loadedMapEntityId = _loadingMapEntityId;
|
||||
_loadingMapEntityId = 0;
|
||||
_currentMap = args.Entity.Logic as MapEntity;
|
||||
if (_currentMap == null)
|
||||
{
|
||||
errorMessage = $"Loaded map entity logic is invalid. EntityId={args.Entity.Id}.";
|
||||
return EventHandleStatus.Failed;
|
||||
}
|
||||
|
||||
return EventHandleStatus.Succeeded;
|
||||
}
|
||||
|
||||
public EventHandleStatus HandleShowEntityFailure(ShowEntityFailureEventArgs args, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (args == null)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
if (args.EntityLogicType != typeof(MapEntity) || args.EntityId != _loadingMapEntityId)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
_loadingMapEntityId = 0;
|
||||
_currentMap = null;
|
||||
errorMessage = $"Map load failed. Asset='{args.EntityAssetName}', Error='{args.ErrorMessage}'.";
|
||||
return EventHandleStatus.Failed;
|
||||
}
|
||||
|
||||
public void HandleHideEntityComplete(HideEntityCompleteEventArgs args)
|
||||
{
|
||||
if (args == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.EntityId == _loadedMapEntityId)
|
||||
{
|
||||
_loadedMapEntityId = 0;
|
||||
_currentMap = null;
|
||||
}
|
||||
|
||||
if (args.EntityId == _loadingMapEntityId)
|
||||
{
|
||||
_loadingMapEntityId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public EventHandleStatus HandleOpenUIFormSuccess(OpenUIFormSuccessEventArgs args, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (args == null || !_loadingCombatInfoFormSerialId.HasValue || args.UIForm == null)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
if (args.UIForm.SerialId != _loadingCombatInfoFormSerialId.Value)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
_isCombatInfoFormReady = args.UIForm.Logic is CombatInfoForm;
|
||||
if (!_isCombatInfoFormReady)
|
||||
{
|
||||
errorMessage = "CombatInfoForm open success but form logic type is invalid.";
|
||||
return EventHandleStatus.Failed;
|
||||
}
|
||||
|
||||
return EventHandleStatus.Succeeded;
|
||||
}
|
||||
|
||||
public EventHandleStatus HandleOpenUIFormFailure(OpenUIFormFailureEventArgs args, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (args == null || !_loadingCombatInfoFormSerialId.HasValue)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
if (args.SerialId != _loadingCombatInfoFormSerialId.Value)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
_isCombatInfoFormReady = false;
|
||||
errorMessage = $"CombatInfoForm load failed. Asset='{args.UIFormAssetName}', Error='{args.ErrorMessage}'.";
|
||||
return EventHandleStatus.Failed;
|
||||
}
|
||||
|
||||
public void HandleCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
|
||||
{
|
||||
if (args == null || !_loadingCombatInfoFormSerialId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.SerialId == _loadingCombatInfoFormSerialId.Value)
|
||||
{
|
||||
_loadingCombatInfoFormSerialId = null;
|
||||
_isCombatInfoFormReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryShowMap(DRLevel level, MapEntityLoadContext mapLoadContext, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (level == null)
|
||||
{
|
||||
errorMessage = "CombatLoadSession map load failed. Level is null.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mapLoadContext?.InitialMapData == null)
|
||||
{
|
||||
errorMessage = "CombatLoadSession map load failed. MapEntityLoadContext is missing initial map data.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string mapAssetName = level.Id.ToString();
|
||||
string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName);
|
||||
if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist)
|
||||
{
|
||||
errorMessage = $"CombatLoadSession map asset not found. Level='{level.Id}', Asset='{mapAssetPath}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
_loadingMapEntityId = _entity.GenerateSerialId();
|
||||
MapData resolvedMapData = mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero);
|
||||
MapEntityLoadContext resolvedLoadContext = new MapEntityLoadContext(
|
||||
resolvedMapData,
|
||||
mapLoadContext.TryConsumeCoin,
|
||||
mapLoadContext.AddCoin);
|
||||
_entity.ShowMap(resolvedLoadContext, mapAssetName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryOpenCombatInfoForm(ICombatSchedulerPort schedulerPort, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (_combatInfoFormUseCase == null)
|
||||
{
|
||||
_combatInfoFormUseCase = new CombatInfoFormUseCase();
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _combatInfoFormUseCase);
|
||||
}
|
||||
|
||||
_combatInfoFormUseCase.Configure(
|
||||
() => BuildCombatInfoFormRawData(schedulerPort),
|
||||
() => schedulerPort != null && schedulerPort.CanEndCombat && schedulerPort.TryEndCombatByPlayer(),
|
||||
() => schedulerPort != null && schedulerPort.TryDebugFail("Manual debug fail from CombatInfoForm."));
|
||||
|
||||
int? serialId = GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
|
||||
if (!serialId.HasValue)
|
||||
{
|
||||
errorMessage = "CombatLoadSession failed to open CombatInfoForm.";
|
||||
return false;
|
||||
}
|
||||
|
||||
_loadingCombatInfoFormSerialId = serialId.Value;
|
||||
_isCombatInfoFormReady = GameEntry.UI.HasUIForm(serialId.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CombatInfoFormRawData BuildCombatInfoFormRawData(ICombatSchedulerPort schedulerPort)
|
||||
{
|
||||
if (schedulerPort == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DRLevel level = schedulerPort.CurrentLevel;
|
||||
LevelThemeType themeType = level != null ? level.LevelThemeType : LevelThemeType.None;
|
||||
int levelId = level != null ? level.Id : 0;
|
||||
int baseHpMax = level != null ? Mathf.Max(0, level.BaseHp) : 0;
|
||||
return CombatInfoFormUseCase.BuildRawData(
|
||||
themeType,
|
||||
levelId,
|
||||
schedulerPort.DisplayPhaseIndex,
|
||||
schedulerPort.PhaseCount,
|
||||
schedulerPort.CurrentCoin,
|
||||
schedulerPort.CurrentBaseHp,
|
||||
baseHpMax,
|
||||
schedulerPort.CanEndCombat);
|
||||
}
|
||||
|
||||
private void CloseCombatInfoForm()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.CombatInfoForm);
|
||||
_loadingCombatInfoFormSerialId = null;
|
||||
_isCombatInfoFormReady = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatRunResourceStore
|
||||
{
|
||||
private readonly List<TowerStatsData> _buildTowerStatsSnapshot = new();
|
||||
private readonly List<TowerItemData> _participantTowerSnapshot = new();
|
||||
private readonly BackpackInventoryData _rewardInventory = new();
|
||||
|
||||
private BackpackInventoryData _combatInventorySnapshot = new();
|
||||
private bool _isCombatActive;
|
||||
|
||||
public int CurrentCoin { get; private set; }
|
||||
public int CurrentGold => _isCombatActive ? GainedGold : 0;
|
||||
public int CurrentBaseHp { get; private set; }
|
||||
public int MaxBaseHp { get; private set; }
|
||||
public int CurrentBuildTowerCount => _isCombatActive ? _buildTowerStatsSnapshot.Count : 0;
|
||||
public int GainedCoin { get; private set; }
|
||||
public int GainedGold { get; private set; }
|
||||
|
||||
public void InitializeForCombat(DRLevel level)
|
||||
{
|
||||
Reset();
|
||||
if (level == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MaxBaseHp = Mathf.Max(0, level.BaseHp);
|
||||
CurrentBaseHp = MaxBaseHp;
|
||||
CurrentCoin = Mathf.Max(0, level.StartCoin);
|
||||
CacheCombatSnapshots();
|
||||
_isCombatActive = true;
|
||||
}
|
||||
|
||||
public void MarkCombatEnded()
|
||||
{
|
||||
_isCombatActive = false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_isCombatActive = false;
|
||||
CurrentCoin = 0;
|
||||
CurrentBaseHp = 0;
|
||||
MaxBaseHp = 0;
|
||||
GainedCoin = 0;
|
||||
GainedGold = 0;
|
||||
_buildTowerStatsSnapshot.Clear();
|
||||
_participantTowerSnapshot.Clear();
|
||||
_combatInventorySnapshot = new BackpackInventoryData();
|
||||
_rewardInventory.Gold = 0;
|
||||
_rewardInventory.MuzzleComponents.Clear();
|
||||
_rewardInventory.BearingComponents.Clear();
|
||||
_rewardInventory.BaseComponents.Clear();
|
||||
_rewardInventory.Towers.Clear();
|
||||
_rewardInventory.ParticipantTowerInstanceIds.Clear();
|
||||
}
|
||||
|
||||
public BackpackInventoryData GetRewardInventorySnapshot()
|
||||
{
|
||||
return InventoryCloneUtility.CloneInventory(_rewardInventory);
|
||||
}
|
||||
|
||||
public BackpackInventoryData GetCombatInventorySnapshot()
|
||||
{
|
||||
return InventoryCloneUtility.CloneInventory(_combatInventorySnapshot);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerItemData> GetParticipantTowerSnapshot()
|
||||
{
|
||||
List<TowerItemData> snapshot = new List<TowerItemData>(_participantTowerSnapshot.Count);
|
||||
for (int i = 0; i < _participantTowerSnapshot.Count; i++)
|
||||
{
|
||||
TowerItemData tower = _participantTowerSnapshot[i];
|
||||
if (tower != null)
|
||||
{
|
||||
snapshot.Add(InventoryCloneUtility.CloneTower(tower));
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public IReadOnlyList<long> GetParticipantTowerInstanceIdSnapshot()
|
||||
{
|
||||
List<long> snapshot = new List<long>(_participantTowerSnapshot.Count);
|
||||
for (int i = 0; i < _participantTowerSnapshot.Count; i++)
|
||||
{
|
||||
TowerItemData tower = _participantTowerSnapshot[i];
|
||||
if (tower != null && tower.InstanceId > 0)
|
||||
{
|
||||
snapshot.Add(tower.InstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public bool TryConsumeCoin(int coin)
|
||||
{
|
||||
int requiredCoin = Mathf.Max(0, coin);
|
||||
if (requiredCoin <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_isCombatActive || CurrentCoin < requiredCoin)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentCoin -= requiredCoin;
|
||||
FireCoinChangedEvent(-requiredCoin);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddCoin(int coin)
|
||||
{
|
||||
int gainedCoin = Mathf.Max(0, coin);
|
||||
if (!_isCombatActive || gainedCoin <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentCoin += gainedCoin;
|
||||
FireCoinChangedEvent(gainedCoin);
|
||||
}
|
||||
|
||||
public int ApplyBaseDamage(int damage)
|
||||
{
|
||||
int resolvedDamage = Mathf.Max(0, damage);
|
||||
if (!_isCombatActive || resolvedDamage <= 0)
|
||||
{
|
||||
return CurrentBaseHp;
|
||||
}
|
||||
|
||||
int previousBaseHp = CurrentBaseHp;
|
||||
CurrentBaseHp = Mathf.Max(0, CurrentBaseHp - resolvedDamage);
|
||||
int deltaBaseHp = CurrentBaseHp - previousBaseHp;
|
||||
if (deltaBaseHp != 0)
|
||||
{
|
||||
FireBaseHpChangedEvent(deltaBaseHp);
|
||||
}
|
||||
|
||||
return CurrentBaseHp;
|
||||
}
|
||||
|
||||
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
|
||||
{
|
||||
stats = null;
|
||||
if (!_isCombatActive || buildIndex < 0 || buildIndex >= _buildTowerStatsSnapshot.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TowerStatsData sourceStats = _buildTowerStatsSnapshot[buildIndex];
|
||||
if (sourceStats == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stats = InventoryCloneUtility.CloneTowerStats(sourceStats);
|
||||
return stats != null;
|
||||
}
|
||||
|
||||
public void AddEnemyDefeatedReward(int gainedCoin, int gainedGold)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int coin = Mathf.Max(0, gainedCoin);
|
||||
int gold = Mathf.Max(0, gainedGold);
|
||||
GainedCoin += coin;
|
||||
GainedGold += gold;
|
||||
if (coin > 0)
|
||||
{
|
||||
CurrentCoin += coin;
|
||||
FireCoinChangedEvent(coin);
|
||||
}
|
||||
|
||||
if (gold > 0)
|
||||
{
|
||||
_rewardInventory.Gold += gold;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddSettlementGold(int gainedGold)
|
||||
{
|
||||
int gold = Mathf.Max(0, gainedGold);
|
||||
if (gold <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GainedGold += gold;
|
||||
_rewardInventory.Gold += gold;
|
||||
}
|
||||
|
||||
public void AddEnemyDefeatedLoot(TowerCompItemData droppedItem)
|
||||
{
|
||||
if (!_isCombatActive || droppedItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AppendRewardItem(droppedItem);
|
||||
}
|
||||
|
||||
private void CacheCombatSnapshots()
|
||||
{
|
||||
_buildTowerStatsSnapshot.Clear();
|
||||
_participantTowerSnapshot.Clear();
|
||||
_combatInventorySnapshot = GameEntry.PlayerInventory != null
|
||||
? GameEntry.PlayerInventory.GetInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
if (GameEntry.PlayerInventory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IReadOnlyList<TowerItemData> towers = GameEntry.PlayerInventory.GetParticipantTowerSnapshot();
|
||||
if (towers == null || towers.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < towers.Count; i++)
|
||||
{
|
||||
TowerItemData tower = towers[i];
|
||||
if (tower == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_participantTowerSnapshot.Add(InventoryCloneUtility.CloneTower(tower));
|
||||
if (tower.Stats != null)
|
||||
{
|
||||
_buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FireCoinChangedEvent(int deltaCoin)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, CombatCoinChangedEventArgs.Create(CurrentCoin, deltaCoin));
|
||||
}
|
||||
|
||||
private void FireBaseHpChangedEvent(int deltaBaseHp)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, CombatBaseHpChangedEventArgs.Create(CurrentBaseHp, deltaBaseHp));
|
||||
}
|
||||
|
||||
private void AppendRewardItem(TowerCompItemData droppedItem)
|
||||
{
|
||||
switch (droppedItem)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
_rewardInventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
_rewardInventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
_rewardInventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public partial class CombatScheduler : ICombatSchedulerPort
|
||||
{
|
||||
private readonly CombatSchedulerRuntime _runtime = new();
|
||||
private readonly CombatSchedulerCoordinator _coordinator;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
public CombatScheduler()
|
||||
{
|
||||
_coordinator = new CombatSchedulerCoordinator(this, _runtime);
|
||||
}
|
||||
|
||||
public bool IsRunning =>
|
||||
_runtime.CurrentState is CombatLoadingState or CombatRunningPhaseState or CombatWaitingForPhaseEndState;
|
||||
|
||||
public bool IsCompleted => _runtime.IsCompleted;
|
||||
public DRLevel CurrentLevel => _runtime.CurrentLevel;
|
||||
public DRLevelPhase CurrentPhase => _runtime.PhaseLoopRuntime.CurrentPhase;
|
||||
public MapEntity CurrentMap => _runtime.LoadSession.CurrentMap;
|
||||
public int DisplayPhaseIndex => _runtime.PhaseLoopRuntime.DisplayPhaseIndex;
|
||||
public int PhaseCount => _runtime.PhaseLoopRuntime.PhaseCount;
|
||||
public bool CanEndCombat => _runtime.PhaseLoopRuntime.CanEndCombat;
|
||||
public int CurrentCoin => _runtime.CombatRunResourceStore.CurrentCoin;
|
||||
public int CurrentGold => _runtime.CombatRunResourceStore.CurrentGold;
|
||||
public int CurrentBaseHp => _runtime.CombatRunResourceStore.CurrentBaseHp;
|
||||
public int CurrentBuildTowerCount => _runtime.CombatRunResourceStore.CurrentBuildTowerCount;
|
||||
public int DefeatedEnemyCount => _runtime.EnemyManager.DefeatedEnemyCount;
|
||||
public int GainedCoin => _runtime.CombatRunResourceStore.GainedCoin;
|
||||
public int GainedGold => _runtime.CombatRunResourceStore.GainedGold;
|
||||
|
||||
public void OnInit(Action<bool> combatEndedCallback)
|
||||
{
|
||||
_runtime.CombatEndedCallback = combatEndedCallback;
|
||||
|
||||
if (!_initialized)
|
||||
{
|
||||
_runtime.Entity = GameEntry.Entity;
|
||||
_runtime.EventBridge.Bind(
|
||||
OnShowEntitySuccess,
|
||||
OnShowEntityFailure,
|
||||
OnHideEntityComplete,
|
||||
OnOpenUIFormSuccess,
|
||||
OnOpenUIFormFailure,
|
||||
OnCloseUIFormComplete);
|
||||
_runtime.EnemyManager.OnInit(this);
|
||||
_runtime.LoadSession.OnInit(_runtime.Entity);
|
||||
_coordinator.EnsureCombatFinishFormUseCaseBound();
|
||||
_coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
_coordinator.ResetRuntime();
|
||||
}
|
||||
|
||||
public bool Start(
|
||||
DRLevel level,
|
||||
IReadOnlyList<DRLevelPhase> phases,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId,
|
||||
RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized || _runtime.Entity == null)
|
||||
{
|
||||
return _coordinator.HandleStartFailure("CombatScheduler start failed. Runtime is not initialized.");
|
||||
}
|
||||
|
||||
if (level == null || phases == null || phases.Count <= 0)
|
||||
{
|
||||
return _coordinator.HandleStartFailure("CombatScheduler start failed. Invalid level or phase data.");
|
||||
}
|
||||
|
||||
_coordinator.CleanupAllCombatEntities();
|
||||
_coordinator.CloseCombatFinishForm();
|
||||
_coordinator.CloseRewardSelectForm();
|
||||
_coordinator.CloseDialogForm();
|
||||
_runtime.EnemyManager.EndPhase();
|
||||
_runtime.EnemyManager.ResetCombatStats();
|
||||
_coordinator.ResetRuntime();
|
||||
_runtime.DidCombatWin = true;
|
||||
|
||||
_runtime.CurrentLevel = level;
|
||||
_runtime.NodeContext = context != null ? context.Clone() : null;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
_runtime.CombatRunResourceStore.InitializeForCombat(level);
|
||||
foreach (var phase in phases)
|
||||
{
|
||||
if (phase != null)
|
||||
{
|
||||
_runtime.PhaseBuffer.Add(phase);
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnEntriesByPhaseId != null)
|
||||
{
|
||||
foreach (var pair in spawnEntriesByPhaseId)
|
||||
{
|
||||
_runtime.SpawnEntriesByPhaseId[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
_runtime.PhaseLoopRuntime.SetPhases(_runtime.PhaseBuffer);
|
||||
if (_runtime.PhaseLoopRuntime.PhaseCount <= 0)
|
||||
{
|
||||
return _coordinator.HandleStartFailure(
|
||||
$"CombatScheduler start failed. Level '{level.Id}' has no phase data.");
|
||||
}
|
||||
|
||||
ChangeState(new CombatLoadingState(_runtime, _coordinator));
|
||||
Log.Info(
|
||||
"CombatScheduler started. Level={0}, PhaseCount={1}.",
|
||||
_runtime.CurrentLevel.Id,
|
||||
_runtime.PhaseLoopRuntime.PhaseCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_runtime.CurrentState?.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtime.CurrentState?.OnExit();
|
||||
_runtime.CurrentState?.OnDestroy();
|
||||
_runtime.CurrentState = null;
|
||||
_coordinator.CleanupAllCombatEntities();
|
||||
_coordinator.CloseCombatFinishForm();
|
||||
_coordinator.CloseRewardSelectForm();
|
||||
_coordinator.CloseDialogForm();
|
||||
_runtime.EnemyManager.OnDestroy();
|
||||
_coordinator.ResetRuntime();
|
||||
_runtime.EventBridge.Unbind();
|
||||
_runtime.CombatFinishFormUseCase = null;
|
||||
_runtime.RewardSelectFormUseCase = null;
|
||||
_runtime.CombatEndedCallback = null;
|
||||
|
||||
_runtime.Entity = null;
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
public bool TryEndCombatByPlayer()
|
||||
{
|
||||
if (_runtime.CurrentState is not CombatRunningPhaseState &&
|
||||
_runtime.CurrentState is not CombatWaitingForPhaseEndState)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _runtime.PhaseLoopRuntime.TryRequestEndCombat();
|
||||
}
|
||||
|
||||
public void OnEnemyReachedBase(DREnemy enemy)
|
||||
{
|
||||
if (!IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int resolvedBaseDamage = enemy != null ? Mathf.Max(0, enemy.BaseDamage) : 0;
|
||||
if (resolvedBaseDamage <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_coordinator.ApplyBaseDamage(resolvedBaseDamage);
|
||||
}
|
||||
|
||||
public void OnEnemyDefeated(DREnemy enemy)
|
||||
{
|
||||
if (!IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyDropContext context = new(
|
||||
enemy,
|
||||
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
_coordinator.ResolveCurrentThemeType());
|
||||
int nextDropOrdinal = _runtime.NextDropOrdinal;
|
||||
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(
|
||||
context,
|
||||
_runtime.NodeContext != null ? _runtime.NodeContext.RunSeed : 0,
|
||||
_runtime.NodeContext != null ? _runtime.NodeContext.SequenceIndex : -1,
|
||||
ref nextDropOrdinal);
|
||||
_runtime.NextDropOrdinal = nextDropOrdinal;
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
||||
}
|
||||
|
||||
public bool OnCombatFinishReturnRequested()
|
||||
{
|
||||
if (_runtime.CurrentState is not CombatWaitingForReturnState waitingForReturnState)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
waitingForReturnState.RequestReturn();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryConsumeCoin(int coin)
|
||||
{
|
||||
return _runtime.CombatRunResourceStore.TryConsumeCoin(coin);
|
||||
}
|
||||
|
||||
public void AddCoin(int coin)
|
||||
{
|
||||
_runtime.CombatRunResourceStore.AddCoin(coin);
|
||||
}
|
||||
|
||||
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
|
||||
{
|
||||
return _runtime.CombatRunResourceStore.TryGetBuildTowerStats(buildIndex, out stats);
|
||||
}
|
||||
|
||||
public bool TryDebugFail(string errorMessage)
|
||||
{
|
||||
if (_runtime.IsCompleted || _runtime.CurrentState == null || _runtime.CurrentState is CombatFailedState)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_coordinator.EnterFailureFallback(string.IsNullOrWhiteSpace(errorMessage)
|
||||
? "Manual debug fail."
|
||||
: errorMessage);
|
||||
return _runtime.CurrentState is CombatFailedState;
|
||||
}
|
||||
|
||||
void ICombatSchedulerPort.ChangeState(CombatStateBase nextState)
|
||||
{
|
||||
ChangeState(nextState);
|
||||
}
|
||||
|
||||
internal void ChangeState(CombatStateBase nextState)
|
||||
{
|
||||
if (ReferenceEquals(_runtime.CurrentState, nextState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtime.CurrentState?.OnExit();
|
||||
_runtime.CurrentState?.OnDestroy();
|
||||
_runtime.CurrentState = nextState;
|
||||
_runtime.CurrentState?.OnInit();
|
||||
_runtime.CurrentState?.OnEnter();
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args)
|
||||
{
|
||||
var status = _runtime.LoadSession.HandleShowEntitySuccess(args, out string errorMessage);
|
||||
if (status == CombatLoadSession.EventHandleStatus.Failed)
|
||||
{
|
||||
_coordinator.EnterFailureFallback(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status == CombatLoadSession.EventHandleStatus.Succeeded)
|
||||
{
|
||||
MapEntity map = _runtime.LoadSession.CurrentMap;
|
||||
Log.Info(
|
||||
"Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.",
|
||||
_runtime.CurrentLevel != null ? _runtime.CurrentLevel.Id : 0,
|
||||
map.PathCells.Count,
|
||||
map.FoundationCells.Count,
|
||||
map.Spawners.Length,
|
||||
map.House != null ? map.House.name : "None");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(ShowEntityFailureEventArgs args)
|
||||
{
|
||||
var status = _runtime.LoadSession.HandleShowEntityFailure(args, out string errorMessage);
|
||||
if (status == CombatLoadSession.EventHandleStatus.Failed)
|
||||
{
|
||||
_coordinator.EnterFailureFallback(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(HideEntityCompleteEventArgs args)
|
||||
{
|
||||
_runtime.LoadSession.HandleHideEntityComplete(args);
|
||||
}
|
||||
|
||||
private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args)
|
||||
{
|
||||
var status = _runtime.LoadSession.HandleOpenUIFormSuccess(args, out string errorMessage);
|
||||
if (status == CombatLoadSession.EventHandleStatus.Failed)
|
||||
{
|
||||
_coordinator.EnterFailureFallback(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args)
|
||||
{
|
||||
var status = _runtime.LoadSession.HandleOpenUIFormFailure(args, out string errorMessage);
|
||||
if (status == CombatLoadSession.EventHandleStatus.Failed)
|
||||
{
|
||||
_coordinator.EnterFailureFallback(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
|
||||
{
|
||||
_runtime.LoadSession.HandleCloseUIFormComplete(args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatSchedulerCoordinator
|
||||
{
|
||||
private readonly ICombatSchedulerPort _schedulerPort;
|
||||
private readonly CombatSchedulerRuntime _runtime;
|
||||
public ICombatSchedulerPort Port => _schedulerPort;
|
||||
|
||||
public CombatSchedulerCoordinator(ICombatSchedulerPort schedulerPort, CombatSchedulerRuntime runtime)
|
||||
{
|
||||
_schedulerPort = schedulerPort;
|
||||
_runtime = runtime;
|
||||
}
|
||||
|
||||
public void ChangeState(CombatStateBase nextState)
|
||||
{
|
||||
_schedulerPort.ChangeState(nextState);
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public void ResetRuntime()
|
||||
{
|
||||
_runtime.CurrentState = null;
|
||||
_runtime.PhaseBuffer.Clear();
|
||||
_runtime.SpawnEntriesByPhaseId.Clear();
|
||||
_runtime.PhaseLoopRuntime.Reset();
|
||||
_runtime.LoadSession.Reset();
|
||||
_runtime.CombatRunResourceStore.Reset();
|
||||
_runtime.SettlementContext = null;
|
||||
_runtime.CurrentLevel = null;
|
||||
_runtime.DidCombatWin = true;
|
||||
_runtime.IsCompleted = false;
|
||||
_runtime.NodeEnterFired = false;
|
||||
_runtime.NodeContext = null;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
}
|
||||
|
||||
public void CleanupAllCombatEntities()
|
||||
{
|
||||
_runtime.LoadSession.Cleanup();
|
||||
_runtime.EnemyManager.CleanupTrackedEnemies();
|
||||
}
|
||||
|
||||
public void EnsureCombatFinishFormUseCaseBound()
|
||||
{
|
||||
_runtime.CombatFinishFormUseCase ??= new CombatFinishFormUseCase(_schedulerPort);
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _runtime.CombatFinishFormUseCase);
|
||||
}
|
||||
|
||||
public void EnsureRewardSelectFormUseCaseBound()
|
||||
{
|
||||
_runtime.RewardSelectFormUseCase ??= new RewardSelectFormUseCase();
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _runtime.RewardSelectFormUseCase);
|
||||
}
|
||||
|
||||
public void OpenCombatFailureDialog(string errorMessage)
|
||||
{
|
||||
CloseDialogForm();
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData
|
||||
{
|
||||
Mode = 1,
|
||||
Title = "Combat Error",
|
||||
Message = string.IsNullOrWhiteSpace(errorMessage) ? "Combat failed unexpectedly." : errorMessage,
|
||||
PauseGame = false,
|
||||
ConfirmText = "Return Menu",
|
||||
OnClickConfirm = OnCombatFailureDialogConfirmed
|
||||
});
|
||||
}
|
||||
|
||||
public bool TryBeginNextPhase()
|
||||
{
|
||||
if (!_runtime.PhaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
|
||||
{
|
||||
_schedulerPort.ChangeState(new CombatSettlementState(_runtime, this, true));
|
||||
return false;
|
||||
}
|
||||
|
||||
_runtime.SpawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries);
|
||||
_schedulerPort.ChangeState(new CombatRunningPhaseState(_runtime, this, nextPhase, spawnEntries));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void EnterWaitingForPhaseEnd()
|
||||
{
|
||||
_schedulerPort.ChangeState(new CombatWaitingForPhaseEndState(_runtime, this));
|
||||
}
|
||||
|
||||
public void CompleteCurrentPhase()
|
||||
{
|
||||
_runtime.EnemyManager.EndPhase();
|
||||
Log.Info(
|
||||
"CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.",
|
||||
_runtime.CurrentLevel != null ? _runtime.CurrentLevel.Id : 0,
|
||||
_runtime.PhaseLoopRuntime.CurrentPhase != null ? _runtime.PhaseLoopRuntime.CurrentPhase.Id : 0,
|
||||
_runtime.PhaseLoopRuntime.CurrentPhaseElapsed);
|
||||
|
||||
TryBeginNextPhase();
|
||||
}
|
||||
|
||||
public bool ShouldEnterSettlementFromActiveState(out bool didCombatWin)
|
||||
{
|
||||
if (GetCurrentBaseHp() <= 0)
|
||||
{
|
||||
didCombatWin = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_runtime.PhaseLoopRuntime.IsEndCombatRequested)
|
||||
{
|
||||
didCombatWin = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
didCombatWin = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward)
|
||||
{
|
||||
if (_runtime.CurrentState is not CombatRewardSelectionState || _runtime.SettlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtime.CombatSettlementService.ApplySelectedReward(_runtime.SettlementContext, selectedReward);
|
||||
_schedulerPort.ChangeState(new CombatFinishFormState(_runtime, this));
|
||||
}
|
||||
|
||||
public void OnFullBaseHpRewardGiveUp()
|
||||
{
|
||||
if (_runtime.CurrentState is not CombatRewardSelectionState || _runtime.SettlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_schedulerPort.ChangeState(new CombatFinishFormState(_runtime, this));
|
||||
}
|
||||
|
||||
public LevelThemeType ResolveCurrentThemeType()
|
||||
{
|
||||
if (_runtime.CurrentLevel != null)
|
||||
{
|
||||
return _runtime.CurrentLevel.LevelThemeType;
|
||||
}
|
||||
|
||||
return LevelThemeType.None;
|
||||
}
|
||||
|
||||
public int ApplyBaseDamage(int damage)
|
||||
{
|
||||
return _runtime.CombatRunResourceStore.ApplyBaseDamage(damage);
|
||||
}
|
||||
|
||||
public int GetCurrentBaseHp()
|
||||
{
|
||||
return Mathf.Max(0, _runtime.CombatRunResourceStore.CurrentBaseHp);
|
||||
}
|
||||
|
||||
public void CloseCombatFinishForm()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
|
||||
}
|
||||
|
||||
public void CloseRewardSelectForm()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm);
|
||||
}
|
||||
|
||||
public void CloseDialogForm()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
|
||||
}
|
||||
|
||||
public void CompleteNormalCombatAndNotify(bool didCombatWin)
|
||||
{
|
||||
CompleteCombat(didCombatWin);
|
||||
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
nodeContext?.RunId,
|
||||
nodeContext?.NodeId ?? 0,
|
||||
nodeContext?.NodeType ?? RunNodeType.None,
|
||||
nodeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
didCombatWin,
|
||||
nodeContext != null
|
||||
? nodeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
}
|
||||
|
||||
public void CompleteFailureCombatAndNotify()
|
||||
{
|
||||
CleanupAllCombatEntities();
|
||||
CloseCombatFinishForm();
|
||||
CloseRewardSelectForm();
|
||||
CloseDialogForm();
|
||||
CompleteCombat(false);
|
||||
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
nodeContext?.RunId,
|
||||
nodeContext?.NodeId ?? 0,
|
||||
nodeContext?.NodeType ?? RunNodeType.None,
|
||||
nodeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Exception,
|
||||
false,
|
||||
nodeContext != null
|
||||
? nodeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
}
|
||||
|
||||
public bool HandleStartFailure(string errorMessage)
|
||||
{
|
||||
Log.Warning("{0}", errorMessage);
|
||||
_runtime.EnemyManager.EndPhase();
|
||||
CleanupAllCombatEntities();
|
||||
CloseCombatFinishForm();
|
||||
CloseRewardSelectForm();
|
||||
CloseDialogForm();
|
||||
ResetRuntime();
|
||||
return false;
|
||||
}
|
||||
|
||||
public void EnterFailureFallback(string errorMessage)
|
||||
{
|
||||
if (_runtime.CurrentState is CombatFailedState || _runtime.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_schedulerPort.ChangeState(new CombatFailedState(_runtime, this, errorMessage));
|
||||
}
|
||||
|
||||
public void OnCombatFailureDialogConfirmed(object userData)
|
||||
{
|
||||
_ = userData;
|
||||
if (_runtime.CurrentState is not CombatFailedState || _runtime.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CompleteFailureCombatAndNotify();
|
||||
}
|
||||
|
||||
private void CompleteCombat(bool succeeded)
|
||||
{
|
||||
_runtime.IsCompleted = true;
|
||||
_runtime.CurrentState = null;
|
||||
_runtime.CombatRunResourceStore.MarkCombatEnded();
|
||||
_runtime.CombatEndedCallback?.Invoke(succeeded);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Procedure;
|
||||
using GeometryTD.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatSchedulerRuntime
|
||||
{
|
||||
public List<DRLevelPhase> PhaseBuffer { get; } = new();
|
||||
public Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> SpawnEntriesByPhaseId { get; } = new();
|
||||
public EnemyManager EnemyManager { get; } = new();
|
||||
public PhaseLoopRuntime PhaseLoopRuntime { get; } = new();
|
||||
public CombatLoadSession LoadSession { get; } = new();
|
||||
public CombatEventBridge EventBridge { get; } = new();
|
||||
public CombatRunResourceStore CombatRunResourceStore { get; } = new();
|
||||
public CombatSettlementService CombatSettlementService { get; } = new();
|
||||
|
||||
public EntityComponent Entity { get; set; }
|
||||
public DRLevel CurrentLevel { get; set; }
|
||||
public CombatFinishFormUseCase CombatFinishFormUseCase { get; set; }
|
||||
public RewardSelectFormUseCase RewardSelectFormUseCase { get; set; }
|
||||
public CombatStateBase CurrentState { get; set; }
|
||||
public Action<bool> CombatEndedCallback { get; set; }
|
||||
public bool DidCombatWin { get; set; } = true;
|
||||
public bool IsCompleted { get; set; }
|
||||
public bool NodeEnterFired { get; set; }
|
||||
public CombatSettlementContext SettlementContext { get; set; }
|
||||
public RunNodeExecutionContext NodeContext { get; set; }
|
||||
public int NextDropOrdinal { get; set; }
|
||||
public int NextRewardOrdinal { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementContext
|
||||
{
|
||||
public CombatSettlementFlags Flags { get; } = new();
|
||||
public CombatSettlementResult Result { get; } = new();
|
||||
public CombatSettlementSummary Summary { get; } = new();
|
||||
}
|
||||
|
||||
public sealed class CombatSettlementFlags
|
||||
{
|
||||
public bool ShouldOpenRewardSelection;
|
||||
public bool DidEnterRewardSelection;
|
||||
public bool IsCommitted;
|
||||
}
|
||||
|
||||
public sealed class CombatSettlementResult
|
||||
{
|
||||
public bool DidCombatWin;
|
||||
public int FinalCoin;
|
||||
public int FinalBaseHp;
|
||||
public int MaxBaseHp;
|
||||
public int DefeatedEnemyCount;
|
||||
public int GainedGold;
|
||||
public BackpackInventoryData RewardInventory;
|
||||
public CombatSettlementEnduranceResult Endurance { get; } = new();
|
||||
}
|
||||
|
||||
public sealed class CombatSettlementEnduranceResult
|
||||
{
|
||||
public List<long> TargetTowerInstanceIds { get; } = new();
|
||||
public float EnduranceLossPerComponent;
|
||||
public int AffectedTowerCount;
|
||||
}
|
||||
|
||||
public sealed class CombatSettlementSummary
|
||||
{
|
||||
public int DefeatedEnemyCount;
|
||||
public int GainedGold;
|
||||
public BackpackInventoryData RewardInventory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementService
|
||||
{
|
||||
private const int RewardSelectDisplayCount = 3;
|
||||
private readonly CombatSettlementCalculator _calculator = new();
|
||||
private readonly CombatSettlementCommitter _committer = new();
|
||||
|
||||
public CombatSettlementContext BuildSettlementContext(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
int defeatedEnemyCount,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
return _calculator.BuildSettlementContext(
|
||||
didCombatWin,
|
||||
currentLevel,
|
||||
defeatedEnemyCount,
|
||||
resourceStore);
|
||||
}
|
||||
|
||||
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
|
||||
{
|
||||
_committer.CommitSettlementInventory(settlementContext);
|
||||
}
|
||||
|
||||
public bool TryPrepareRewardSelection(
|
||||
CombatSettlementContext settlementContext,
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextRewardOrdinal,
|
||||
RewardSelectFormUseCase rewardSelectFormUseCase,
|
||||
Action<RewardSelectItemRawData> onRewardSelected,
|
||||
Action onGiveUp)
|
||||
{
|
||||
if (settlementContext == null || rewardSelectFormUseCase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
RewardSelectDisplayCount,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
ref nextRewardOrdinal);
|
||||
if (candidateItems == null || candidateItems.Count <= 0)
|
||||
{
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
rewardSelectFormUseCase.SetCallbacks(onRewardSelected, onGiveUp);
|
||||
rewardSelectFormUseCase.ConfigureRewardCandidates(
|
||||
candidateItems,
|
||||
displayCount: RewardSelectDisplayCount,
|
||||
refreshCost: 0,
|
||||
allowRotateOnce: false,
|
||||
allowGiveUp: false,
|
||||
tipText: "基地满血奖励:请选择 1 个组件");
|
||||
|
||||
RewardSelectFormRawData rawData = rewardSelectFormUseCase.CreateInitialModel();
|
||||
if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0)
|
||||
{
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
settlementContext.Flags.DidEnterRewardSelection = true;
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.RewardSelectForm, rawData);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
|
||||
{
|
||||
_committer.ApplySelectedReward(settlementContext, selectedReward);
|
||||
}
|
||||
|
||||
public void OpenCombatFinishForm(
|
||||
CombatSettlementContext settlementContext,
|
||||
CombatFinishFormUseCase combatFinishFormUseCase)
|
||||
{
|
||||
if (settlementContext == null || combatFinishFormUseCase == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
combatFinishFormUseCase.SetSummary(settlementContext);
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatFailedState : CombatStateBase
|
||||
{
|
||||
private readonly string _errorMessage;
|
||||
|
||||
public CombatFailedState(
|
||||
CombatSchedulerRuntime runtime,
|
||||
CombatSchedulerCoordinator coordinator,
|
||||
string errorMessage) : base(runtime, coordinator)
|
||||
{
|
||||
_errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
Log.Error(
|
||||
"CombatScheduler failed. LevelId={0}, {1}",
|
||||
Runtime.CurrentLevel != null ? Runtime.CurrentLevel.Id : 0,
|
||||
_errorMessage);
|
||||
Runtime.EnemyManager.EndPhase();
|
||||
Coordinator.CloseCombatFinishForm();
|
||||
Coordinator.CloseRewardSelectForm();
|
||||
Coordinator.OpenCombatFailureDialog(_errorMessage);
|
||||
}
|
||||
|
||||
public override void OnExit()
|
||||
{
|
||||
Coordinator.CloseDialogForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatFinishFormState : CombatStateBase
|
||||
{
|
||||
public CombatFinishFormState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (Runtime.SettlementContext == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("Combat finish form failed. Settlement context is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.EnsureCombatFinishFormUseCaseBound();
|
||||
Runtime.CombatSettlementService.OpenCombatFinishForm(
|
||||
Runtime.SettlementContext,
|
||||
Runtime.CombatFinishFormUseCase);
|
||||
Coordinator.ChangeState(new CombatWaitingForReturnState(Runtime, Coordinator));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatLoadingState : CombatStateBase
|
||||
{
|
||||
public CombatLoadingState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (Runtime.CurrentLevel == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("Combat loading failed. Current level is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
MapEntityLoadContext mapLoadContext = BuildMapLoadContext();
|
||||
if (!Runtime.LoadSession.StartLoading(Runtime.CurrentLevel, mapLoadContext, Coordinator.Port, out string errorMessage))
|
||||
{
|
||||
Coordinator.EnterFailureFallback($"Combat loading failed. {errorMessage}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_ = elapseSeconds;
|
||||
_ = realElapseSeconds;
|
||||
|
||||
if (!Runtime.LoadSession.IsReady)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.TryBeginNextPhase();
|
||||
}
|
||||
|
||||
private MapEntityLoadContext BuildMapLoadContext()
|
||||
{
|
||||
List<TowerStatsData> buildTowerStatsSnapshot = new();
|
||||
for (int i = 0; i < Runtime.CombatRunResourceStore.CurrentBuildTowerCount; i++)
|
||||
{
|
||||
if (Runtime.CombatRunResourceStore.TryGetBuildTowerStats(i, out TowerStatsData stats) &&
|
||||
stats != null)
|
||||
{
|
||||
buildTowerStatsSnapshot.Add(stats);
|
||||
}
|
||||
}
|
||||
|
||||
MapData mapData = new MapData(
|
||||
entityId: 0,
|
||||
typeId: 0,
|
||||
levelId: Runtime.CurrentLevel.Id,
|
||||
position: Vector3.zero,
|
||||
initialCoin: Runtime.CombatRunResourceStore.CurrentCoin,
|
||||
buildTowerStatsSnapshot: buildTowerStatsSnapshot,
|
||||
inventorySnapshot: Runtime.CombatRunResourceStore.GetCombatInventorySnapshot(),
|
||||
participantTowerSnapshot: Runtime.CombatRunResourceStore.GetParticipantTowerSnapshot());
|
||||
return new MapEntityLoadContext(
|
||||
mapData,
|
||||
Runtime.CombatRunResourceStore.TryConsumeCoin,
|
||||
Runtime.CombatRunResourceStore.AddCoin);
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatRewardSelectionState : CombatStateBase
|
||||
{
|
||||
public CombatRewardSelectionState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (Runtime.SettlementContext == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("Combat reward selection failed. Settlement context is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||
int nextRewardOrdinal = Runtime.NextRewardOrdinal;
|
||||
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
||||
Runtime.SettlementContext,
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Coordinator.ResolveCurrentThemeType(),
|
||||
Runtime.NodeContext != null ? Runtime.NodeContext.RunSeed : 0,
|
||||
Runtime.NodeContext != null ? Runtime.NodeContext.SequenceIndex : -1,
|
||||
ref nextRewardOrdinal,
|
||||
Runtime.RewardSelectFormUseCase,
|
||||
Coordinator.OnFullBaseHpRewardSelected,
|
||||
Coordinator.OnFullBaseHpRewardGiveUp))
|
||||
{
|
||||
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.NextRewardOrdinal = nextRewardOrdinal;
|
||||
}
|
||||
|
||||
public override void OnExit()
|
||||
{
|
||||
Coordinator.CloseRewardSelectForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatRunningPhaseState : CombatStateBase
|
||||
{
|
||||
private readonly DRLevelPhase _phase;
|
||||
private readonly IReadOnlyList<DRLevelSpawnEntry> _spawnEntries;
|
||||
|
||||
public CombatRunningPhaseState(
|
||||
CombatSchedulerRuntime runtime,
|
||||
CombatSchedulerCoordinator coordinator,
|
||||
DRLevelPhase phase,
|
||||
IReadOnlyList<DRLevelSpawnEntry> spawnEntries) : base(runtime, coordinator)
|
||||
{
|
||||
_phase = phase;
|
||||
_spawnEntries = spawnEntries;
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
Runtime.EnemyManager.BeginPhase(_phase, _spawnEntries);
|
||||
GameEntry.Event.Fire(
|
||||
Coordinator,
|
||||
CombatProcessEventArgs.Create(
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Runtime.PhaseLoopRuntime.PhaseCount));
|
||||
GameEntry.Event.Fire(
|
||||
Coordinator,
|
||||
CombatEnemyHpRateChangedEventArgs.Create(
|
||||
Coordinator.ResolveEnemyHpRateMultiplier(
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Runtime.PhaseLoopRuntime.PhaseCount)));
|
||||
|
||||
if (!Runtime.NodeEnterFired)
|
||||
{
|
||||
Runtime.NodeEnterFired = true;
|
||||
GameEntry.Event.Fire(
|
||||
Coordinator,
|
||||
NodeEnterEventArgs.Create(
|
||||
Runtime.NodeContext?.RunId,
|
||||
Runtime.NodeContext?.NodeId ?? 0,
|
||||
Runtime.NodeContext?.NodeType ?? RunNodeType.None,
|
||||
Runtime.NodeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
"CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.",
|
||||
Runtime.CurrentLevel != null ? Runtime.CurrentLevel.Id : 0,
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
_phase.EndType,
|
||||
_spawnEntries != null ? _spawnEntries.Count : 0);
|
||||
}
|
||||
|
||||
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (Runtime.PhaseLoopRuntime.CurrentPhase == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("CombatScheduler update failed. Current phase is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
|
||||
Runtime.EnemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
if (Coordinator.ShouldEnterSettlementFromActiveState(out bool didCombatWin))
|
||||
{
|
||||
Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, didCombatWin));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Runtime.EnemyManager.IsPhaseSpawnCompleted)
|
||||
{
|
||||
Coordinator.EnterWaitingForPhaseEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatSettlementState : CombatStateBase
|
||||
{
|
||||
private readonly bool _didCombatWin;
|
||||
|
||||
public CombatSettlementState(
|
||||
CombatSchedulerRuntime runtime,
|
||||
CombatSchedulerCoordinator coordinator,
|
||||
bool didCombatWin) : base(runtime, coordinator)
|
||||
{
|
||||
_didCombatWin = didCombatWin;
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
Runtime.EnemyManager.EndPhase();
|
||||
Runtime.EnemyManager.CleanupTrackedEnemies();
|
||||
Runtime.DidCombatWin = _didCombatWin;
|
||||
Runtime.SettlementContext = Runtime.CombatSettlementService.BuildSettlementContext(
|
||||
_didCombatWin,
|
||||
Runtime.CurrentLevel,
|
||||
Runtime.EnemyManager.DefeatedEnemyCount,
|
||||
Runtime.CombatRunResourceStore);
|
||||
if (Runtime.SettlementContext.Flags.ShouldOpenRewardSelection)
|
||||
{
|
||||
Coordinator.ChangeState(new CombatRewardSelectionState(Runtime, Coordinator));
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal abstract class CombatStateBase
|
||||
{
|
||||
protected CombatSchedulerRuntime Runtime { get; }
|
||||
protected CombatSchedulerCoordinator Coordinator { get; }
|
||||
|
||||
protected CombatStateBase(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
{
|
||||
Runtime = runtime;
|
||||
Coordinator = coordinator;
|
||||
}
|
||||
|
||||
public virtual void OnInit()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnEnter()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnExit()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnDestroy()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Factory;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatWaitingForPhaseEndState : CombatStateBase
|
||||
{
|
||||
public CombatWaitingForPhaseEndState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_ = realElapseSeconds;
|
||||
|
||||
DRLevelPhase currentPhase = Runtime.PhaseLoopRuntime.CurrentPhase;
|
||||
if (currentPhase == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("CombatScheduler waiting phase failed. Current phase is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
|
||||
|
||||
if (Coordinator.ShouldEnterSettlementFromActiveState(out bool didCombatWin))
|
||||
{
|
||||
Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, didCombatWin));
|
||||
return;
|
||||
}
|
||||
|
||||
PhaseEndConditionContext conditionContext = new(
|
||||
currentPhase,
|
||||
Runtime.PhaseLoopRuntime.CurrentPhaseElapsed,
|
||||
Runtime.EnemyManager.IsPhaseSpawnCompleted,
|
||||
Runtime.EnemyManager.AliveEnemyCount,
|
||||
Runtime.EnemyManager.HasAliveBoss);
|
||||
IPhaseEndCondition endCondition = PhaseEndConditionFactory.Create(currentPhase.EndType);
|
||||
if (!endCondition.ShouldExit(conditionContext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.CompleteCurrentPhase();
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatWaitingForReturnState : CombatStateBase
|
||||
{
|
||||
private bool _returnRequested;
|
||||
|
||||
public CombatWaitingForReturnState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public void RequestReturn()
|
||||
{
|
||||
_returnRequested = true;
|
||||
}
|
||||
|
||||
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_ = elapseSeconds;
|
||||
_ = realElapseSeconds;
|
||||
|
||||
if (!_returnRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Runtime.SettlementContext == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("Combat return failed. Settlement context is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.CombatSettlementService.CommitSettlementInventory(Runtime.SettlementContext);
|
||||
Runtime.LoadSession.Cleanup();
|
||||
Coordinator.CloseCombatFinishForm();
|
||||
Coordinator.CloseRewardSelectForm();
|
||||
Coordinator.CompleteNormalCombatAndNotify(Runtime.DidCombatWin);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public readonly struct EnemyDropContext
|
||||
{
|
||||
public EnemyDropContext(DREnemy enemy, int displayPhaseIndex, LevelThemeType themeType)
|
||||
{
|
||||
Enemy = enemy;
|
||||
DisplayPhaseIndex = displayPhaseIndex;
|
||||
ThemeType = themeType;
|
||||
}
|
||||
|
||||
public DREnemy Enemy { get; }
|
||||
|
||||
public int DisplayPhaseIndex { get; }
|
||||
|
||||
public LevelThemeType ThemeType { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public readonly struct EnemyDropResult
|
||||
{
|
||||
public static EnemyDropResult Empty => new(0, 0, null);
|
||||
|
||||
public EnemyDropResult(int coin, int gold, TowerCompItemData lootItem)
|
||||
{
|
||||
Coin = coin;
|
||||
Gold = gold;
|
||||
LootItem = lootItem;
|
||||
}
|
||||
|
||||
public int Coin { get; }
|
||||
|
||||
public int Gold { get; }
|
||||
|
||||
public TowerCompItemData LootItem { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using GeometryTD.DataTable;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal interface ICombatSchedulerPort
|
||||
{
|
||||
DRLevel CurrentLevel { get; }
|
||||
int DisplayPhaseIndex { get; }
|
||||
int PhaseCount { get; }
|
||||
int CurrentCoin { get; }
|
||||
int CurrentBaseHp { get; }
|
||||
bool CanEndCombat { get; }
|
||||
|
||||
void ChangeState(CombatStateBase nextState);
|
||||
bool TryEndCombatByPlayer();
|
||||
bool TryDebugFail(string errorMessage);
|
||||
bool OnCombatFinishReturnRequested();
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class BossDeadPhaseEndCondition : IPhaseEndCondition
|
||||
{
|
||||
public PhaseEndType EndType => PhaseEndType.BossDead;
|
||||
|
||||
public bool ShouldExit(in PhaseEndConditionContext context)
|
||||
{
|
||||
return context.IsPhaseSpawnCompleted && !context.HasAliveBoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class EnemiesClearedPhaseEndCondition : IPhaseEndCondition
|
||||
{
|
||||
public PhaseEndType EndType => PhaseEndType.EnemiesCleared;
|
||||
|
||||
public bool ShouldExit(in PhaseEndConditionContext context)
|
||||
{
|
||||
return context.IsPhaseSpawnCompleted && context.AliveEnemyCount <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal interface IPhaseEndCondition
|
||||
{
|
||||
PhaseEndType EndType { get; }
|
||||
|
||||
bool ShouldExit(in PhaseEndConditionContext context);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class NonePhaseEndCondition : IPhaseEndCondition
|
||||
{
|
||||
public PhaseEndType EndType => PhaseEndType.None;
|
||||
|
||||
public bool ShouldExit(in PhaseEndConditionContext context)
|
||||
{
|
||||
if (context.Phase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Phase.DurationSeconds > 0 &&
|
||||
context.PhaseElapsedSeconds >= context.Phase.DurationSeconds)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return context.IsPhaseSpawnCompleted && context.AliveEnemyCount <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using GeometryTD.DataTable;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal readonly struct PhaseEndConditionContext
|
||||
{
|
||||
public PhaseEndConditionContext(
|
||||
DRLevelPhase phase,
|
||||
float phaseElapsedSeconds,
|
||||
bool isPhaseSpawnCompleted,
|
||||
int aliveEnemyCount,
|
||||
bool hasAliveBoss)
|
||||
{
|
||||
Phase = phase;
|
||||
PhaseElapsedSeconds = phaseElapsedSeconds;
|
||||
IsPhaseSpawnCompleted = isPhaseSpawnCompleted;
|
||||
AliveEnemyCount = aliveEnemyCount;
|
||||
HasAliveBoss = hasAliveBoss;
|
||||
}
|
||||
|
||||
public DRLevelPhase Phase { get; }
|
||||
|
||||
public float PhaseElapsedSeconds { get; }
|
||||
|
||||
public bool IsPhaseSpawnCompleted { get; }
|
||||
|
||||
public int AliveEnemyCount { get; }
|
||||
|
||||
public bool HasAliveBoss { get; }
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
using System.Globalization;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class TimeElapsedPhaseEndCondition : IPhaseEndCondition
|
||||
{
|
||||
public PhaseEndType EndType => PhaseEndType.TimeElapsed;
|
||||
|
||||
public bool ShouldExit(in PhaseEndConditionContext context)
|
||||
{
|
||||
if (context.Phase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.PhaseElapsedSeconds >= ResolveTimeElapsedThreshold(context.Phase);
|
||||
}
|
||||
|
||||
private static float ResolveTimeElapsedThreshold(DRLevelPhase phase)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(phase.EndParam) &&
|
||||
float.TryParse(phase.EndParam, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
if (phase.DurationSeconds > 0)
|
||||
{
|
||||
return phase.DurationSeconds;
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class PhaseLoopRuntime
|
||||
{
|
||||
private readonly List<DRLevelPhase> _phases = new();
|
||||
|
||||
private DRLevelPhase _currentPhase;
|
||||
private int _currentPhaseIndex = -1;
|
||||
private int _displayPhaseIndex;
|
||||
private int _completedLoopCount;
|
||||
private bool _canEndCombat;
|
||||
private bool _endCombatRequested;
|
||||
private float _currentPhaseElapsed;
|
||||
|
||||
public DRLevelPhase CurrentPhase => _currentPhase;
|
||||
public int DisplayPhaseIndex => _displayPhaseIndex;
|
||||
public bool CanEndCombat => _canEndCombat;
|
||||
public bool IsEndCombatRequested => _endCombatRequested;
|
||||
public float CurrentPhaseElapsed => _currentPhaseElapsed;
|
||||
public int PhaseCount => _phases.Count;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_phases.Clear();
|
||||
_currentPhase = null;
|
||||
_currentPhaseIndex = -1;
|
||||
_displayPhaseIndex = 0;
|
||||
_completedLoopCount = 0;
|
||||
_canEndCombat = false;
|
||||
_endCombatRequested = false;
|
||||
_currentPhaseElapsed = 0f;
|
||||
}
|
||||
|
||||
public void SetPhases(IReadOnlyList<DRLevelPhase> phases)
|
||||
{
|
||||
_phases.Clear();
|
||||
if (phases == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < phases.Count; i++)
|
||||
{
|
||||
DRLevelPhase phase = phases[i];
|
||||
if (phase != null)
|
||||
{
|
||||
_phases.Add(phase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRequestEndCombat()
|
||||
{
|
||||
if (!_canEndCombat)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_endCombatRequested = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AdvancePhaseElapsed(float elapseSeconds)
|
||||
{
|
||||
_currentPhaseElapsed += elapseSeconds;
|
||||
}
|
||||
|
||||
public bool TryEnterNextPhase(out DRLevelPhase nextPhase)
|
||||
{
|
||||
nextPhase = null;
|
||||
if (_phases.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentPhaseIndex++;
|
||||
if (_currentPhaseIndex >= _phases.Count)
|
||||
{
|
||||
_completedLoopCount++;
|
||||
_canEndCombat = true;
|
||||
|
||||
if (_endCombatRequested)
|
||||
{
|
||||
_currentPhase = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentPhaseIndex = 0;
|
||||
}
|
||||
|
||||
_currentPhase = _phases[_currentPhaseIndex];
|
||||
_displayPhaseIndex = _completedLoopCount * _phases.Count + _currentPhaseIndex + 1;
|
||||
_currentPhaseElapsed = 0f;
|
||||
nextPhase = _currentPhase;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user