init
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class BuiltinDataComponent : GameFrameworkComponent
|
||||
{
|
||||
[SerializeField] private TextAsset _buildInfoTextAsset = null;
|
||||
|
||||
[SerializeField] private UpdateResourceForm _updateResourceFormTemplate = null;
|
||||
|
||||
private BuildInfo _buildInfo = null;
|
||||
|
||||
public BuildInfo BuildInfo => _buildInfo;
|
||||
|
||||
public UpdateResourceForm UpdateResourceFormTemplate => _updateResourceFormTemplate;
|
||||
|
||||
public void InitBuildInfo()
|
||||
{
|
||||
if (_buildInfoTextAsset == null || string.IsNullOrEmpty(_buildInfoTextAsset.text))
|
||||
{
|
||||
Log.Info("Build info can not be found or empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
_buildInfo = Utility.Json.ToObject<BuildInfo>(_buildInfoTextAsset.text);
|
||||
if (_buildInfo == null)
|
||||
{
|
||||
Log.Warning("Parse build info failure.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// 鎴樻枟鑺傜偣缁勪欢
|
||||
/// </summary>
|
||||
public class CombatNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
// Level.Id => Level
|
||||
private readonly Dictionary<int, DRLevel> _levelsById = new();
|
||||
|
||||
// LevelId => LevelPhases
|
||||
private readonly Dictionary<int, List<DRLevelPhase>> _phasesByLevelId = new();
|
||||
|
||||
// LevelPhase.Id => LevelSpawnEntries
|
||||
private readonly Dictionary<int, List<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
|
||||
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _selectedSpawnEntriesByPhaseId = new();
|
||||
private readonly List<int> _levelIdBuffer = new();
|
||||
private readonly CombatScheduler _combatScheduler = new CombatScheduler();
|
||||
|
||||
private bool _runtimeInitialized;
|
||||
private bool _isCombatActive;
|
||||
private bool _lastCombatSucceeded = true;
|
||||
|
||||
public LevelThemeType CurrentThemeType { get; private set; }
|
||||
public DRLevel CurrentLevel { get; private set; }
|
||||
public int CurrentCoin => _isCombatActive ? _combatScheduler.CurrentCoin : 0;
|
||||
public int CurrentGold => _isCombatActive ? _combatScheduler.CurrentGold : 0;
|
||||
public int CurrentBaseHp => _isCombatActive ? _combatScheduler.CurrentBaseHp : 0;
|
||||
public bool LastCombatSucceeded => _lastCombatSucceeded;
|
||||
public bool CanEndCombat => _combatScheduler.CanEndCombat;
|
||||
public int CurrentBuildTowerCount => _isCombatActive ? _combatScheduler.CurrentBuildTowerCount : 0;
|
||||
public int LastDefeatedEnemyCount { get; private set; }
|
||||
public int LastGainedCoin { get; private set; }
|
||||
public int LastGainedGold { get; private set; }
|
||||
|
||||
public int CurrentLevelPhaseCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CurrentLevel == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!_phasesByLevelId.TryGetValue(CurrentLevel.Id, out List<DRLevelPhase> phases) || phases == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return phases.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public int CurrentPhaseIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return _combatScheduler.DisplayPhaseIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnInit(LevelThemeType themeType)
|
||||
{
|
||||
ShutdownCombatRuntime();
|
||||
|
||||
CurrentThemeType = themeType;
|
||||
CurrentLevel = null;
|
||||
_isCombatActive = false;
|
||||
_lastCombatSucceeded = true;
|
||||
LastDefeatedEnemyCount = 0;
|
||||
LastGainedCoin = 0;
|
||||
LastGainedGold = 0;
|
||||
_levelsById.Clear();
|
||||
_phasesByLevelId.Clear();
|
||||
_spawnEntriesByPhaseId.Clear();
|
||||
_selectedSpawnEntriesByPhaseId.Clear();
|
||||
_levelIdBuffer.Clear();
|
||||
|
||||
IDataTable<DRLevel> dtLevel = GameEntry.DataTable.GetDataTable<DRLevel>();
|
||||
IDataTable<DRLevelPhase> dtLevelPhase = GameEntry.DataTable.GetDataTable<DRLevelPhase>();
|
||||
IDataTable<DRLevelSpawnEntry> dtSpawnEntry = GameEntry.DataTable.GetDataTable<DRLevelSpawnEntry>();
|
||||
if (dtLevel == null || dtLevelPhase == null || dtSpawnEntry == null)
|
||||
{
|
||||
Log.Warning("CombatNodeComponent init failed. Missing data table(s).");
|
||||
return;
|
||||
}
|
||||
|
||||
DRLevel[] levels = dtLevel.GetAllDataRows();
|
||||
for (int i = 0; i < levels.Length; i++)
|
||||
{
|
||||
DRLevel level = levels[i];
|
||||
if (level.LevelThemeType != themeType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_levelsById[level.Id] = level;
|
||||
_phasesByLevelId[level.Id] = new List<DRLevelPhase>();
|
||||
_levelIdBuffer.Add(level.Id);
|
||||
}
|
||||
|
||||
DRLevelPhase[] levelPhases = dtLevelPhase.GetAllDataRows();
|
||||
foreach (var phase in levelPhases)
|
||||
{
|
||||
int levelId = phase.Id / 1000;
|
||||
if (!_levelsById.ContainsKey(levelId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_phasesByLevelId.TryGetValue(levelId, out List<DRLevelPhase> phases))
|
||||
{
|
||||
phases = new List<DRLevelPhase>();
|
||||
_phasesByLevelId[levelId] = phases;
|
||||
}
|
||||
|
||||
phases.Add(phase);
|
||||
_spawnEntriesByPhaseId[phase.Id] = new List<DRLevelSpawnEntry>();
|
||||
}
|
||||
|
||||
DRLevelSpawnEntry[] spawnEntries = dtSpawnEntry.GetAllDataRows();
|
||||
foreach (var spawnEntry in spawnEntries)
|
||||
{
|
||||
int phaseId = spawnEntry.Id / 1000;
|
||||
int levelId = phaseId / 1000;
|
||||
if (!_levelsById.ContainsKey(levelId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_spawnEntriesByPhaseId.TryGetValue(phaseId, out List<DRLevelSpawnEntry> entries))
|
||||
{
|
||||
entries = new List<DRLevelSpawnEntry>();
|
||||
_spawnEntriesByPhaseId[phaseId] = entries;
|
||||
}
|
||||
|
||||
entries.Add(spawnEntry);
|
||||
}
|
||||
|
||||
foreach (List<DRLevelPhase> phaseList in _phasesByLevelId.Values)
|
||||
{
|
||||
phaseList.Sort((left, right) => left.Id.CompareTo(right.Id));
|
||||
}
|
||||
|
||||
foreach (List<DRLevelSpawnEntry> phaseSpawnEntries in _spawnEntriesByPhaseId.Values)
|
||||
{
|
||||
phaseSpawnEntries.Sort((left, right) =>
|
||||
{
|
||||
int timeCompare = left.StartTime.CompareTo(right.StartTime);
|
||||
if (timeCompare != 0)
|
||||
{
|
||||
return timeCompare;
|
||||
}
|
||||
|
||||
return left.Id.CompareTo(right.Id);
|
||||
});
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
"CombatNodeComponent initialized. Theme={0}, Levels={1}, Phases={2}, Entries={3}.",
|
||||
themeType,
|
||||
_levelsById.Count,
|
||||
CountPhases(),
|
||||
CountEntries());
|
||||
|
||||
EnsureCombatRuntimeInitialized();
|
||||
}
|
||||
|
||||
public void StartCombat(
|
||||
int levelId = 0,
|
||||
RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!EnsureCombatRuntimeInitialized())
|
||||
{
|
||||
Log.Warning("CombatNodeComponent start failed. Missing scheduler runtime.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasLevel = levelId > 0
|
||||
? TryGetLevelById(levelId, out DRLevel selectedLevel)
|
||||
: TrySelectRandomLevel(out selectedLevel);
|
||||
if (!hasLevel)
|
||||
{
|
||||
Log.Warning(
|
||||
"CombatNodeComponent start failed. LevelId={0}. Missing cached level data or invalid level id.",
|
||||
levelId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_phasesByLevelId.TryGetValue(selectedLevel.Id, out List<DRLevelPhase> phaseList) ||
|
||||
phaseList == null ||
|
||||
phaseList.Count <= 0)
|
||||
{
|
||||
Log.Warning("CombatNodeComponent start failed. Level '{0}' has no phase data.", selectedLevel.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSpawnEntriesByPhaseId.Clear();
|
||||
foreach (var phase in phaseList)
|
||||
{
|
||||
if (_spawnEntriesByPhaseId.TryGetValue(phase.Id, out List<DRLevelSpawnEntry> entries) &&
|
||||
entries != null)
|
||||
{
|
||||
_selectedSpawnEntriesByPhaseId[phase.Id] = entries;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedSpawnEntriesByPhaseId[phase.Id] = new List<DRLevelSpawnEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
CurrentLevel = selectedLevel;
|
||||
_isCombatActive = false;
|
||||
_lastCombatSucceeded = true;
|
||||
LastDefeatedEnemyCount = 0;
|
||||
LastGainedCoin = 0;
|
||||
LastGainedGold = 0;
|
||||
if (!_combatScheduler.Start(
|
||||
selectedLevel,
|
||||
phaseList,
|
||||
_selectedSpawnEntriesByPhaseId,
|
||||
context))
|
||||
{
|
||||
CurrentLevel = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_isCombatActive = true;
|
||||
}
|
||||
|
||||
public void EndCombat()
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isCombatActive = false;
|
||||
LastDefeatedEnemyCount = _combatScheduler.DefeatedEnemyCount;
|
||||
LastGainedCoin = _combatScheduler.GainedCoin;
|
||||
LastGainedGold = _combatScheduler.GainedGold;
|
||||
CurrentLevel = null;
|
||||
}
|
||||
|
||||
public bool TryEndCombatByPlayer()
|
||||
{
|
||||
return _combatScheduler.TryEndCombatByPlayer();
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (!_runtimeInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_combatScheduler.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
public void OnShutdown()
|
||||
{
|
||||
_isCombatActive = false;
|
||||
CurrentLevel = null;
|
||||
_lastCombatSucceeded = true;
|
||||
LastDefeatedEnemyCount = 0;
|
||||
LastGainedCoin = 0;
|
||||
LastGainedGold = 0;
|
||||
ShutdownCombatRuntime();
|
||||
}
|
||||
|
||||
public void OnCombatEndedByScheduler(bool succeeded)
|
||||
{
|
||||
_lastCombatSucceeded = succeeded;
|
||||
EndCombat();
|
||||
}
|
||||
|
||||
public bool TryConsumeCoin(int coin)
|
||||
{
|
||||
if (Mathf.Max(0, coin) <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _combatScheduler.TryConsumeCoin(coin);
|
||||
}
|
||||
|
||||
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
stats = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return _combatScheduler.TryGetBuildTowerStats(buildIndex, out stats);
|
||||
}
|
||||
|
||||
public void AddCoin(int coin)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_combatScheduler.AddCoin(coin);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OnShutdown();
|
||||
}
|
||||
|
||||
private bool EnsureCombatRuntimeInitialized()
|
||||
{
|
||||
if (_runtimeInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_combatScheduler.OnInit(OnCombatEndedByScheduler);
|
||||
_runtimeInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ShutdownCombatRuntime()
|
||||
{
|
||||
if (!_runtimeInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_combatScheduler.OnDestroy();
|
||||
_runtimeInitialized = false;
|
||||
}
|
||||
|
||||
private bool TrySelectRandomLevel(out DRLevel level)
|
||||
{
|
||||
level = null;
|
||||
if (_levelIdBuffer.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int selectedIndex = Random.Range(0, _levelIdBuffer.Count);
|
||||
int selectedLevelId = _levelIdBuffer[selectedIndex];
|
||||
return _levelsById.TryGetValue(selectedLevelId, out level);
|
||||
}
|
||||
|
||||
private bool TryGetLevelById(int levelId, out DRLevel level)
|
||||
{
|
||||
level = null;
|
||||
if (levelId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _levelsById.TryGetValue(levelId, out level);
|
||||
}
|
||||
|
||||
private int CountPhases()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (List<DRLevelPhase> phases in _phasesByLevelId.Values)
|
||||
{
|
||||
count += phases.Count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private int CountEntries()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (List<DRLevelSpawnEntry> list in _spawnEntriesByPhaseId.Values)
|
||||
{
|
||||
count += list.Count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementCalculator
|
||||
{
|
||||
private const float FullBaseHpGoldBonusRate = 0.3f;
|
||||
private const float HighBaseHpGoldBonusRate = 0.1f;
|
||||
private const float HighBaseHpThreshold = 0.8f;
|
||||
private const float SettlementTowerEnduranceLoss = 1f;
|
||||
|
||||
public CombatSettlementContext BuildSettlementContext(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
int defeatedEnemyCount,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
bool shouldOpenFullBaseHpRewardSelect = false;
|
||||
ResolveSettlementByBaseHp(
|
||||
didCombatWin,
|
||||
currentLevel,
|
||||
resourceStore,
|
||||
out int currentBaseHp,
|
||||
out int maxBaseHp,
|
||||
out int levelRewardGold,
|
||||
out float bonusRate,
|
||||
out int bonusGold,
|
||||
out shouldOpenFullBaseHpRewardSelect);
|
||||
|
||||
CombatSettlementContext settlementContext = new CombatSettlementContext();
|
||||
settlementContext.Result.DidCombatWin = didCombatWin;
|
||||
settlementContext.Result.FinalCoin = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentCoin : 0);
|
||||
settlementContext.Result.FinalBaseHp = currentBaseHp;
|
||||
settlementContext.Result.MaxBaseHp = maxBaseHp;
|
||||
settlementContext.Result.DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount);
|
||||
settlementContext.Result.GainedGold = Mathf.Max(0, resourceStore != null ? resourceStore.GainedGold : 0);
|
||||
settlementContext.Result.RewardInventory = resourceStore != null
|
||||
? resourceStore.GetRewardInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
PopulateEnduranceSettlement(settlementContext, resourceStore);
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect;
|
||||
settlementContext.Flags.DidEnterRewardSelection = false;
|
||||
settlementContext.Summary.DefeatedEnemyCount = settlementContext.Result.DefeatedEnemyCount;
|
||||
settlementContext.Summary.GainedGold = settlementContext.Result.GainedGold;
|
||||
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
|
||||
|
||||
Log.Info(
|
||||
"Combat settlement resolved. Level={0}, BaseHp={1}/{2}, LevelReward={3}, BonusRate={4:P0}, BonusGold={5}, FullHpRewardSelect={6}, EnduranceTargets={7}.",
|
||||
currentLevel != null ? currentLevel.Id : 0,
|
||||
currentBaseHp,
|
||||
maxBaseHp,
|
||||
levelRewardGold,
|
||||
bonusRate,
|
||||
bonusGold,
|
||||
shouldOpenFullBaseHpRewardSelect,
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count);
|
||||
return settlementContext;
|
||||
}
|
||||
|
||||
private static void ResolveSettlementByBaseHp(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
CombatRunResourceStore resourceStore,
|
||||
out int currentBaseHp,
|
||||
out int maxBaseHp,
|
||||
out int levelRewardGold,
|
||||
out float bonusRate,
|
||||
out int bonusGold,
|
||||
out bool shouldOpenFullBaseHpRewardSelect)
|
||||
{
|
||||
currentBaseHp = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentBaseHp : 0);
|
||||
maxBaseHp = resourceStore != null ? Mathf.Max(0, resourceStore.MaxBaseHp) : 0;
|
||||
if (maxBaseHp > 0)
|
||||
{
|
||||
currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp);
|
||||
}
|
||||
|
||||
levelRewardGold = currentLevel != null ? Mathf.Max(0, currentLevel.RewardGold) : 0;
|
||||
bonusRate = 0f;
|
||||
bonusGold = 0;
|
||||
shouldOpenFullBaseHpRewardSelect = false;
|
||||
|
||||
if (!didCombatWin || resourceStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxBaseHp > 0 && currentBaseHp >= maxBaseHp)
|
||||
{
|
||||
bonusRate = FullBaseHpGoldBonusRate;
|
||||
shouldOpenFullBaseHpRewardSelect = true;
|
||||
}
|
||||
else if (maxBaseHp > 0)
|
||||
{
|
||||
float hpRate = (float)currentBaseHp / maxBaseHp;
|
||||
if (hpRate >= HighBaseHpThreshold)
|
||||
{
|
||||
bonusRate = HighBaseHpGoldBonusRate;
|
||||
}
|
||||
}
|
||||
|
||||
int goldForBonusCalculation = Mathf.Max(0, resourceStore.GainedGold) + levelRewardGold;
|
||||
bonusGold = bonusRate > 0f ? Mathf.FloorToInt(goldForBonusCalculation * bonusRate) : 0;
|
||||
int settlementGold = levelRewardGold + bonusGold;
|
||||
resourceStore.AddSettlementGold(settlementGold);
|
||||
}
|
||||
|
||||
private static void PopulateEnduranceSettlement(
|
||||
CombatSettlementContext settlementContext,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
if (settlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CombatSettlementEnduranceResult enduranceResult = settlementContext.Result.Endurance;
|
||||
enduranceResult.TargetTowerInstanceIds.Clear();
|
||||
enduranceResult.EnduranceLossPerComponent = SettlementTowerEnduranceLoss;
|
||||
|
||||
IReadOnlyList<long> participantTowerIds = resourceStore?.GetParticipantTowerInstanceIdSnapshot();
|
||||
if (participantTowerIds == null || participantTowerIds.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < participantTowerIds.Count; i++)
|
||||
{
|
||||
long towerId = participantTowerIds[i];
|
||||
if (towerId > 0)
|
||||
{
|
||||
enduranceResult.TargetTowerInstanceIds.Add(towerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementCommitter
|
||||
{
|
||||
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
|
||||
{
|
||||
if (settlementContext == null || settlementContext.Flags.IsCommitted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BackpackInventoryData rewardInventory = settlementContext.Result.RewardInventory ?? new BackpackInventoryData();
|
||||
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
|
||||
settlementContext.Result.RewardInventory = rewardInventory;
|
||||
settlementContext.Summary.RewardInventory = rewardInventory;
|
||||
settlementContext.Result.Endurance.AffectedTowerCount = ApplyDeferredSettlementEndurance(settlementContext);
|
||||
settlementContext.Flags.IsCommitted = true;
|
||||
}
|
||||
|
||||
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
|
||||
{
|
||||
if (settlementContext?.Result.RewardInventory == null || selectedReward?.SourceItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryAppendRewardComponent(settlementContext.Result.RewardInventory, selectedReward.SourceItem);
|
||||
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
|
||||
}
|
||||
|
||||
private static int ApplyDeferredSettlementEndurance(CombatSettlementContext settlementContext)
|
||||
{
|
||||
if (settlementContext == null ||
|
||||
settlementContext.Result.Endurance.EnduranceLossPerComponent <= 0f ||
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
PlayerInventoryComponent inventory = GameEntry.PlayerInventory;
|
||||
if (inventory == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return inventory.ReduceTowerEndurance(
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds,
|
||||
settlementContext.Result.Endurance.EnduranceLossPerComponent);
|
||||
}
|
||||
|
||||
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)
|
||||
{
|
||||
if (rewardInventory == null || selectedItem == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedItem is MuzzleCompItemData muzzleComp)
|
||||
{
|
||||
rewardInventory.MuzzleComponents.Add(muzzleComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedItem is BearingCompItemData bearingComp)
|
||||
{
|
||||
rewardInventory.BearingComponents.Add(bearingComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedItem is BaseCompItemData baseComp)
|
||||
{
|
||||
rewardInventory.BaseComponents.Add(baseComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class EnemyConfigProvider
|
||||
{
|
||||
private const int DefaultEnemyConfigId = 1;
|
||||
|
||||
private IDataTable<DREnemy> _drEnemy;
|
||||
private bool _enemyConfigMissingLogged;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_drEnemy = null;
|
||||
_enemyConfigMissingLogged = false;
|
||||
}
|
||||
|
||||
public DREnemy GetEnemyConfig(int enemyId)
|
||||
{
|
||||
if (_drEnemy == null)
|
||||
{
|
||||
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
|
||||
if (_drEnemy == null)
|
||||
{
|
||||
if (!_enemyConfigMissingLogged)
|
||||
{
|
||||
Log.Warning("EnemyConfigProvider can not find DREnemy data table.");
|
||||
_enemyConfigMissingLogged = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (enemyId > 0)
|
||||
{
|
||||
DREnemy targetConfig = _drEnemy.GetDataRow(enemyId);
|
||||
if (targetConfig != null)
|
||||
{
|
||||
return targetConfig;
|
||||
}
|
||||
}
|
||||
|
||||
DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId);
|
||||
if (defaultConfig != null)
|
||||
{
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
DREnemy[] allConfigs = _drEnemy.GetAllDataRows();
|
||||
if (allConfigs.Length > 0)
|
||||
{
|
||||
return allConfigs[0];
|
||||
}
|
||||
|
||||
if (!_enemyConfigMissingLogged)
|
||||
{
|
||||
Log.Warning("EnemyConfigProvider found no enemy configs.");
|
||||
_enemyConfigMissingLogged = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int ResolveScaledEnemyBaseHp(int baseHp, CombatScheduler combatScheduler)
|
||||
{
|
||||
int resolvedBaseHp = Mathf.Max(1, baseHp);
|
||||
int completedLoopCount = ResolveCompletedLoopCount(combatScheduler);
|
||||
if (completedLoopCount <= 0)
|
||||
{
|
||||
return resolvedBaseHp;
|
||||
}
|
||||
|
||||
double scaled = resolvedBaseHp * Math.Pow(2d, completedLoopCount);
|
||||
if (scaled >= int.MaxValue)
|
||||
{
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
return Math.Max(1, (int)Math.Round(scaled));
|
||||
}
|
||||
|
||||
private static int ResolveCompletedLoopCount(CombatScheduler combatScheduler)
|
||||
{
|
||||
if (combatScheduler == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int phaseCount = combatScheduler.PhaseCount;
|
||||
int displayPhaseIndex = combatScheduler.DisplayPhaseIndex;
|
||||
if (phaseCount <= 0 || displayPhaseIndex <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class EnemyLifecycleTracker
|
||||
{
|
||||
private readonly HashSet<int> _aliveBossEntityIds = new();
|
||||
private readonly HashSet<int> _trackedEnemyEntityIds = new();
|
||||
private readonly Dictionary<int, DREnemy> _trackedEnemyConfigByEntityId = new();
|
||||
private readonly Dictionary<int, bool> _bossFlagsByEntityId = new();
|
||||
|
||||
public int AliveEnemyCount { get; private set; }
|
||||
public bool HasAliveBoss => _aliveBossEntityIds.Count > 0;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_aliveBossEntityIds.Clear();
|
||||
_bossFlagsByEntityId.Clear();
|
||||
_trackedEnemyEntityIds.Clear();
|
||||
_trackedEnemyConfigByEntityId.Clear();
|
||||
AliveEnemyCount = 0;
|
||||
}
|
||||
|
||||
public void TrackEnemy(int entityId, DREnemy enemyConfig, bool isBoss)
|
||||
{
|
||||
_trackedEnemyEntityIds.Add(entityId);
|
||||
_trackedEnemyConfigByEntityId[entityId] = enemyConfig;
|
||||
_bossFlagsByEntityId[entityId] = isBoss;
|
||||
}
|
||||
|
||||
public bool Contains(int entityId)
|
||||
{
|
||||
return _trackedEnemyEntityIds.Contains(entityId);
|
||||
}
|
||||
|
||||
public void HandleShowSuccess(int entityId)
|
||||
{
|
||||
if (_trackedEnemyEntityIds.Contains(entityId))
|
||||
{
|
||||
AliveEnemyCount++;
|
||||
if (_bossFlagsByEntityId.TryGetValue(entityId, out bool isBoss) && isBoss)
|
||||
{
|
||||
_aliveBossEntityIds.Add(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleShowFailure(int entityId)
|
||||
{
|
||||
_aliveBossEntityIds.Remove(entityId);
|
||||
_bossFlagsByEntityId.Remove(entityId);
|
||||
_trackedEnemyEntityIds.Remove(entityId);
|
||||
_trackedEnemyConfigByEntityId.Remove(entityId);
|
||||
}
|
||||
|
||||
public bool TryHandleHideComplete(int entityId, out DREnemy enemyConfig)
|
||||
{
|
||||
enemyConfig = null;
|
||||
if (!_trackedEnemyEntityIds.Remove(entityId))
|
||||
{
|
||||
_aliveBossEntityIds.Remove(entityId);
|
||||
_bossFlagsByEntityId.Remove(entityId);
|
||||
_trackedEnemyConfigByEntityId.Remove(entityId);
|
||||
return false;
|
||||
}
|
||||
|
||||
_trackedEnemyConfigByEntityId.TryGetValue(entityId, out enemyConfig);
|
||||
_aliveBossEntityIds.Remove(entityId);
|
||||
_bossFlagsByEntityId.Remove(entityId);
|
||||
_trackedEnemyConfigByEntityId.Remove(entityId);
|
||||
AliveEnemyCount = Mathf.Max(0, AliveEnemyCount - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CopyTrackedEntityIdsTo(List<int> buffer)
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.Clear();
|
||||
foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds)
|
||||
{
|
||||
buffer.Add(trackedEnemyEntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class EnemyManager
|
||||
{
|
||||
private readonly List<int> _trackedEnemyIdBuffer = new();
|
||||
private readonly EnemySpawnDirector _enemySpawnDirector = new();
|
||||
private readonly EnemyConfigProvider _enemyConfigProvider = new();
|
||||
private readonly EnemySpawnPathResolver _enemySpawnPathResolver = new();
|
||||
private readonly EnemyLifecycleTracker _enemyLifecycleTracker = new();
|
||||
|
||||
private CombatScheduler _combatScheduler;
|
||||
private EntityComponent _entity;
|
||||
|
||||
private int _defeatedEnemyCount;
|
||||
private bool _initialized;
|
||||
|
||||
public int AliveEnemyCount => _enemyLifecycleTracker.AliveEnemyCount;
|
||||
public int DefeatedEnemyCount => _defeatedEnemyCount;
|
||||
public bool HasAliveBoss => _enemyLifecycleTracker.HasAliveBoss;
|
||||
public bool IsPhaseSpawnCompleted => _enemySpawnDirector.IsPhaseSpawnCompleted;
|
||||
public bool IsPhaseRunning => _enemySpawnDirector.IsPhaseRunning;
|
||||
|
||||
public void OnInit(CombatScheduler combatScheduler)
|
||||
{
|
||||
_combatScheduler = combatScheduler;
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entity = GameEntry.Entity;
|
||||
_defeatedEnemyCount = 0;
|
||||
_enemySpawnDirector.Reset();
|
||||
_enemyConfigProvider.Reset();
|
||||
_enemySpawnPathResolver.Reset();
|
||||
_trackedEnemyIdBuffer.Clear();
|
||||
_enemyLifecycleTracker.Reset();
|
||||
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void BeginPhase(DRLevelPhase phase, IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
|
||||
{
|
||||
if (!_initialized || _combatScheduler == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = phase;
|
||||
EndPhase();
|
||||
_enemySpawnPathResolver.RefreshCache(_combatScheduler, true);
|
||||
_enemySpawnDirector.BeginPhase(spawnEntries);
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (!_initialized || _combatScheduler == null || !_enemySpawnDirector.IsPhaseRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enemySpawnPathResolver.RefreshCache(_combatScheduler, false);
|
||||
_enemySpawnDirector.OnUpdate(elapseSeconds, SpawnEnemies);
|
||||
}
|
||||
|
||||
public void EndPhase()
|
||||
{
|
||||
_enemySpawnDirector.EndPhase();
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
_combatScheduler = null;
|
||||
return;
|
||||
}
|
||||
|
||||
CleanupTrackedEnemies();
|
||||
EndPhase();
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
|
||||
_enemySpawnPathResolver.Reset();
|
||||
_trackedEnemyIdBuffer.Clear();
|
||||
_enemyLifecycleTracker.Reset();
|
||||
_defeatedEnemyCount = 0;
|
||||
_enemyConfigProvider.Reset();
|
||||
_combatScheduler = null;
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
public void ResetCombatStats()
|
||||
{
|
||||
_defeatedEnemyCount = 0;
|
||||
}
|
||||
|
||||
public void CleanupTrackedEnemies()
|
||||
{
|
||||
_enemyLifecycleTracker.CopyTrackedEntityIdsTo(_trackedEnemyIdBuffer);
|
||||
if (_trackedEnemyIdBuffer.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enemyLifecycleTracker.Reset();
|
||||
|
||||
if (_entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++)
|
||||
{
|
||||
int trackedEnemyEntityId = _trackedEnemyIdBuffer[i];
|
||||
if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId))
|
||||
{
|
||||
_entity.HideEntity(trackedEnemyEntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount)
|
||||
{
|
||||
if (spawnCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_enemySpawnPathResolver.TryResolveSpawnPath(_combatScheduler, entry.SpawnPointId, out IReadOnlyList<Vector3> pathPoints))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DREnemy enemyConfig = _enemyConfigProvider.GetEnemyConfig(entry.EnemyId);
|
||||
if (enemyConfig == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int scaledBaseHp = _enemyConfigProvider.ResolveScaledEnemyBaseHp(enemyConfig.BaseHp, _combatScheduler);
|
||||
bool isBoss = entry.EntryType == Definition.EntryType.Boss;
|
||||
|
||||
for (int i = 0; i < spawnCount; i++)
|
||||
{
|
||||
int enemyEntityId = _entity.GenerateSerialId();
|
||||
_enemyLifecycleTracker.TrackEnemy(enemyEntityId, enemyConfig, isBoss);
|
||||
EnemyData enemyData = new EnemyData(
|
||||
enemyEntityId,
|
||||
enemyConfig.EntityId,
|
||||
pathPoints[0],
|
||||
scaledBaseHp,
|
||||
enemyConfig.Speed,
|
||||
pathPoints);
|
||||
_entity.ShowEnemy(enemyData);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is ShowEntitySuccessEventArgs ne)) return;
|
||||
|
||||
if (ne.EntityLogicType == typeof(EnemyEntity) &&
|
||||
_enemyLifecycleTracker.Contains(ne.Entity.Id))
|
||||
{
|
||||
_enemyLifecycleTracker.HandleShowSuccess(ne.Entity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is ShowEntityFailureEventArgs ne))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ne.EntityLogicType != typeof(EnemyEntity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enemyLifecycleTracker.HandleShowFailure(ne.EntityId);
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is HideEntityCompleteEventArgs ne))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_enemyLifecycleTracker.TryHandleHideComplete(ne.EntityId, out DREnemy enemyConfig))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId);
|
||||
bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning;
|
||||
|
||||
if (isCombatRunning && wasKilled && enemyConfig != null)
|
||||
{
|
||||
_defeatedEnemyCount++;
|
||||
_combatScheduler.OnEnemyDefeated(enemyConfig);
|
||||
}
|
||||
else if (isCombatRunning && !wasKilled && enemyConfig != null)
|
||||
{
|
||||
_combatScheduler.OnEnemyReachedBase(enemyConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class EnemySpawnDirector
|
||||
{
|
||||
private sealed class SpawnEntryRuntime
|
||||
{
|
||||
public DRLevelSpawnEntry Entry;
|
||||
public bool Completed;
|
||||
public float NextTriggerTime;
|
||||
public float EndTime;
|
||||
public int RemainingCount;
|
||||
}
|
||||
|
||||
private const float MinStreamInterval = 0.05f;
|
||||
private const float MinBurstGap = 0.01f;
|
||||
|
||||
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new();
|
||||
private float _phaseElapsed;
|
||||
|
||||
public bool IsPhaseSpawnCompleted { get; private set; } = true;
|
||||
public bool IsPhaseRunning { get; private set; }
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
EndPhase();
|
||||
}
|
||||
|
||||
public void BeginPhase(IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
|
||||
{
|
||||
EndPhase();
|
||||
|
||||
_phaseElapsed = 0f;
|
||||
IsPhaseRunning = true;
|
||||
IsPhaseSpawnCompleted = false;
|
||||
if (spawnEntries != null)
|
||||
{
|
||||
for (int i = 0; i < spawnEntries.Count; i++)
|
||||
{
|
||||
SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]);
|
||||
if (runtime != null)
|
||||
{
|
||||
_spawnRuntimes.Add(runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0;
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, Action<DRLevelSpawnEntry, int> spawnAction)
|
||||
{
|
||||
if (!IsPhaseRunning || spawnAction == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_phaseElapsed += elapseSeconds;
|
||||
UpdateSpawnRuntimes(spawnAction);
|
||||
}
|
||||
|
||||
public void EndPhase()
|
||||
{
|
||||
IsPhaseRunning = false;
|
||||
_phaseElapsed = 0f;
|
||||
_spawnRuntimes.Clear();
|
||||
IsPhaseSpawnCompleted = true;
|
||||
}
|
||||
|
||||
private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry)
|
||||
{
|
||||
if (entry == null || entry.EntryType == EntryType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
SpawnEntryRuntime runtime = new SpawnEntryRuntime
|
||||
{
|
||||
Entry = entry,
|
||||
Completed = false,
|
||||
NextTriggerTime = Mathf.Max(0f, entry.StartTime),
|
||||
EndTime = Mathf.Max(0f, entry.StartTime),
|
||||
RemainingCount = Mathf.Max(0, entry.Count)
|
||||
};
|
||||
|
||||
switch (entry.EntryType)
|
||||
{
|
||||
case EntryType.Stream:
|
||||
{
|
||||
float duration = Mathf.Max(0f, entry.Duration);
|
||||
runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime;
|
||||
runtime.Completed = entry.Count <= 0;
|
||||
return runtime;
|
||||
}
|
||||
case EntryType.Burst:
|
||||
case EntryType.Boss:
|
||||
runtime.Completed = runtime.RemainingCount <= 0;
|
||||
return runtime;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpawnRuntimes(Action<DRLevelSpawnEntry, int> spawnAction)
|
||||
{
|
||||
bool allCompleted = true;
|
||||
for (int i = 0; i < _spawnRuntimes.Count; i++)
|
||||
{
|
||||
SpawnEntryRuntime runtime = _spawnRuntimes[i];
|
||||
if (runtime.Completed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (runtime.Entry.EntryType)
|
||||
{
|
||||
case EntryType.Stream:
|
||||
ProcessStreamRuntime(runtime, spawnAction);
|
||||
break;
|
||||
case EntryType.Burst:
|
||||
case EntryType.Boss:
|
||||
ProcessBurstRuntime(runtime, spawnAction);
|
||||
break;
|
||||
default:
|
||||
runtime.Completed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!runtime.Completed)
|
||||
{
|
||||
allCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
IsPhaseSpawnCompleted = allCompleted;
|
||||
}
|
||||
|
||||
private void ProcessStreamRuntime(SpawnEntryRuntime runtime, Action<DRLevelSpawnEntry, int> spawnAction)
|
||||
{
|
||||
if (_phaseElapsed < runtime.NextTriggerTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int countPerWave = Mathf.Max(0, runtime.Entry.Count);
|
||||
if (countPerWave <= 0)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval;
|
||||
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime)
|
||||
{
|
||||
spawnAction(runtime.Entry, countPerWave);
|
||||
runtime.NextTriggerTime += interval;
|
||||
}
|
||||
|
||||
if (runtime.NextTriggerTime > runtime.EndTime)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessBurstRuntime(SpawnEntryRuntime runtime, Action<DRLevelSpawnEntry, int> spawnAction)
|
||||
{
|
||||
if (_phaseElapsed < runtime.NextTriggerTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtime.RemainingCount <= 0)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
float gap = runtime.Entry.Gap;
|
||||
if (gap <= 0f)
|
||||
{
|
||||
spawnAction(runtime.Entry, runtime.RemainingCount);
|
||||
runtime.RemainingCount = 0;
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
gap = Mathf.Max(gap, MinBurstGap);
|
||||
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0)
|
||||
{
|
||||
spawnAction(runtime.Entry, 1);
|
||||
runtime.RemainingCount--;
|
||||
runtime.NextTriggerTime += gap;
|
||||
}
|
||||
|
||||
runtime.Completed = runtime.RemainingCount <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Map;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class EnemySpawnPathResolver
|
||||
{
|
||||
private readonly List<Spawner> _spawners = new();
|
||||
private readonly Dictionary<int, Spawner> _spawnerByOrder = new();
|
||||
private readonly List<Vector3> _pathBuffer = new();
|
||||
|
||||
private int _nextSpawnerIndex;
|
||||
private int _currentMapEntityId;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_spawners.Clear();
|
||||
_spawnerByOrder.Clear();
|
||||
_pathBuffer.Clear();
|
||||
_nextSpawnerIndex = 0;
|
||||
_currentMapEntityId = 0;
|
||||
}
|
||||
|
||||
public void RefreshCache(CombatScheduler combatScheduler, bool force)
|
||||
{
|
||||
MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null;
|
||||
if (currentMap == null)
|
||||
{
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_spawners.Clear();
|
||||
_spawnerByOrder.Clear();
|
||||
_nextSpawnerIndex = 0;
|
||||
_currentMapEntityId = currentMap.Id;
|
||||
|
||||
Spawner[] mapSpawners = currentMap.Spawners;
|
||||
for (int i = 0; i < mapSpawners.Length; i++)
|
||||
{
|
||||
Spawner spawner = mapSpawners[i];
|
||||
if (spawner == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!currentMap.TryGetDefaultPathCells(spawner, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_spawners.Add(spawner);
|
||||
if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder))
|
||||
{
|
||||
_spawnerByOrder[spawner.SpawnOrder] = spawner;
|
||||
}
|
||||
}
|
||||
|
||||
_spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder));
|
||||
}
|
||||
|
||||
public bool TryResolveSpawnPath(CombatScheduler combatScheduler, int spawnPointId, out IReadOnlyList<Vector3> pathPoints)
|
||||
{
|
||||
pathPoints = null;
|
||||
MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null;
|
||||
if (currentMap == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Spawner spawner = ResolveSpawner(spawnPointId);
|
||||
if (spawner == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) || _pathBuffer.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pathPoints = _pathBuffer;
|
||||
return true;
|
||||
}
|
||||
|
||||
private Spawner ResolveSpawner(int spawnPointId)
|
||||
{
|
||||
if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner))
|
||||
{
|
||||
return mappedSpawner;
|
||||
}
|
||||
|
||||
if (_spawners.Count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count];
|
||||
_nextSpawnerIndex++;
|
||||
return fallbackSpawner;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.Procedure;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class EventNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
private EventItem _activeEvent;
|
||||
|
||||
private readonly List<EventItem> _eventItems = new List<EventItem>();
|
||||
|
||||
private EventFormUseCase _eventFormUseCase;
|
||||
private bool _initialized;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
_eventItems.Clear();
|
||||
|
||||
IDataTable<DREvent> dtEvent = GameEntry.DataTable.GetDataTable<DREvent>();
|
||||
if (dtEvent == null)
|
||||
{
|
||||
Log.Warning("Event data table is not loaded.");
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
DREvent[] rows = dtEvent.GetAllDataRows();
|
||||
foreach (var drEvent in rows)
|
||||
{
|
||||
EventOption[] options = ParseOptions(drEvent.OptionsRaw);
|
||||
_eventItems.Add(new EventItem(drEvent.Id, drEvent.Title, drEvent.Description, options));
|
||||
}
|
||||
|
||||
if (_eventFormUseCase == null)
|
||||
{
|
||||
_eventFormUseCase = new EventFormUseCase();
|
||||
}
|
||||
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.EventForm, _eventFormUseCase);
|
||||
|
||||
_initialized = true;
|
||||
Log.Info("EventNodeComponent initialized with {0} events.", _eventItems.Count);
|
||||
}
|
||||
|
||||
public void StartEvent(RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
OnInit();
|
||||
}
|
||||
|
||||
if (_eventItems.Count <= 0)
|
||||
{
|
||||
Log.Warning("EventNodeComponent has no event data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_eventFormUseCase == null)
|
||||
{
|
||||
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
_activeEvent = SelectActiveEvent(_activeContext);
|
||||
if (_activeEvent == null)
|
||||
{
|
||||
Log.Warning("EventNodeComponent StartEvent failed. No active event could be resolved.");
|
||||
return;
|
||||
}
|
||||
|
||||
_eventFormUseCase.BindEvent(_activeEvent, _activeContext);
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.EventForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeEnterEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
public void EndEvent()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.EventForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
_activeContext != null
|
||||
? _activeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
ClearActiveNodeContext();
|
||||
}
|
||||
|
||||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeContext = null;
|
||||
_activeEvent = null;
|
||||
_eventFormUseCase?.Clear();
|
||||
}
|
||||
|
||||
private static EventOption[] ParseOptions(string optionsRaw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(optionsRaw))
|
||||
{
|
||||
return System.Array.Empty<EventOption>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
JArray array = JArray.Parse(optionsRaw);
|
||||
List<EventOption> options = new List<EventOption>(array.Count);
|
||||
|
||||
for (int i = 0; i < array.Count; i++)
|
||||
{
|
||||
if (!(array[i] is JObject optionObj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string optionText = optionObj.Value<string>("optionText") ?? string.Empty;
|
||||
float probability = optionObj.Value<float?>("probability") ?? 1f;
|
||||
|
||||
EventRequirementBase[] requirements = ParseRequirements(optionObj["requirements"] as JArray);
|
||||
EventEffectBase[] costEffects = ParseEffects(optionObj["costEffects"] as JArray, probability);
|
||||
EventEffectBase[] rewardEffects = ParseEffects(optionObj["rewardEffects"] as JArray, probability);
|
||||
|
||||
options.Add(new EventOption(optionText, requirements, costEffects, rewardEffects, probability));
|
||||
}
|
||||
|
||||
return options.ToArray();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Warning("Failed to parse event options json. {0}", e.Message);
|
||||
return System.Array.Empty<EventOption>();
|
||||
}
|
||||
}
|
||||
|
||||
private static EventRequirementBase[] ParseRequirements(JArray requirementsArray)
|
||||
{
|
||||
if (requirementsArray == null || requirementsArray.Count == 0)
|
||||
{
|
||||
return System.Array.Empty<EventRequirementBase>();
|
||||
}
|
||||
|
||||
List<EventRequirementBase> requirements = new List<EventRequirementBase>(requirementsArray.Count);
|
||||
for (int i = 0; i < requirementsArray.Count; i++)
|
||||
{
|
||||
if (!(requirementsArray[i] is JObject reqObj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string type = reqObj.Value<string>("type");
|
||||
JObject param = reqObj["param"] as JObject;
|
||||
EventRequirementBase requirement = EventRequirementFactory.Create(type, param);
|
||||
if (requirement != null)
|
||||
{
|
||||
requirements.Add(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
return requirements.ToArray();
|
||||
}
|
||||
|
||||
private static EventEffectBase[] ParseEffects(JArray effectsArray, float probability)
|
||||
{
|
||||
if (effectsArray == null || effectsArray.Count == 0)
|
||||
{
|
||||
return System.Array.Empty<EventEffectBase>();
|
||||
}
|
||||
|
||||
List<EventEffectBase> effects = new List<EventEffectBase>(effectsArray.Count);
|
||||
for (int i = 0; i < effectsArray.Count; i++)
|
||||
{
|
||||
if (!(effectsArray[i] is JObject effectObj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string type = effectObj.Value<string>("type");
|
||||
JObject param = effectObj["param"] as JObject;
|
||||
EventEffectBase effect = EventEffectFactory.Create(type, param, probability);
|
||||
if (effect != null)
|
||||
{
|
||||
effects.Add(effect);
|
||||
}
|
||||
}
|
||||
|
||||
return effects.ToArray();
|
||||
}
|
||||
|
||||
private EventItem SelectActiveEvent(RunNodeExecutionContext context)
|
||||
{
|
||||
if (_eventItems.Count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
int randomIndex = Random.Range(0, _eventItems.Count);
|
||||
return _eventItems[randomIndex];
|
||||
}
|
||||
|
||||
System.Random random = new System.Random(BuildSelectionSeed(context));
|
||||
return _eventItems[random.Next(0, _eventItems.Count)];
|
||||
}
|
||||
|
||||
private static int BuildSelectionSeed(RunNodeExecutionContext context)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + context.RunSeed;
|
||||
seed = seed * 31 + context.SequenceIndex;
|
||||
seed = seed * 31 + context.NodeId;
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using GameFramework.ObjectPool;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD;
|
||||
using GeometryTD.PoolObjectBase;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class HPBarComponent : GameFrameworkComponent
|
||||
{
|
||||
[FormerlySerializedAs("m_HPBarItemTemplate")] [SerializeField]
|
||||
private HPBarItem _hpBarItemTemplate = null;
|
||||
|
||||
[FormerlySerializedAs("m_HPBarInstanceRoot")] [SerializeField]
|
||||
private Transform _hpBarInstanceRoot = null;
|
||||
|
||||
[FormerlySerializedAs("m_InstancePoolCapacity")] [SerializeField]
|
||||
private int _instancePoolCapacity = 16;
|
||||
|
||||
private IObjectPool<HPBarItemObject> _hpBarItemObjectPool = null;
|
||||
private List<HPBarItem> _activeHPBarItems = null;
|
||||
private Canvas _cachedCanvas = null;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_hpBarInstanceRoot == null)
|
||||
{
|
||||
Log.Error("You must set HP bar instance root first.");
|
||||
return;
|
||||
}
|
||||
|
||||
_cachedCanvas = _hpBarInstanceRoot.GetComponent<Canvas>();
|
||||
_hpBarItemObjectPool =
|
||||
GameEntry.ObjectPool.CreateSingleSpawnObjectPool<HPBarItemObject>("HPBarItem", _instancePoolCapacity);
|
||||
_activeHPBarItems = new List<HPBarItem>();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
for (int i = _activeHPBarItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
HPBarItem hpBarItem = _activeHPBarItems[i];
|
||||
if (hpBarItem.Refresh())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
HideHPBar(hpBarItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowHPBar(EntityBase entity, float fromHPRatio, float toHPRatio)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
Log.Warning("Entity is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
HPBarItem hpBarItem = GetActiveHPBarItem(entity);
|
||||
if (hpBarItem == null)
|
||||
{
|
||||
hpBarItem = CreateHPBarItem(entity);
|
||||
_activeHPBarItems.Add(hpBarItem);
|
||||
}
|
||||
|
||||
hpBarItem.Init(entity, _cachedCanvas, fromHPRatio, toHPRatio);
|
||||
}
|
||||
|
||||
private void HideHPBar(HPBarItem hpBarItem)
|
||||
{
|
||||
hpBarItem.Reset();
|
||||
_activeHPBarItems.Remove(hpBarItem);
|
||||
_hpBarItemObjectPool.Unspawn(hpBarItem);
|
||||
}
|
||||
|
||||
private HPBarItem GetActiveHPBarItem(EntityBase entity)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _activeHPBarItems.Count; i++)
|
||||
{
|
||||
if (_activeHPBarItems[i].Owner == entity)
|
||||
{
|
||||
return _activeHPBarItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HPBarItem CreateHPBarItem(EntityBase entity)
|
||||
{
|
||||
HPBarItem hpBarItem = null;
|
||||
HPBarItemObject hpBarItemObject = _hpBarItemObjectPool.Spawn();
|
||||
if (hpBarItemObject != null)
|
||||
{
|
||||
hpBarItem = (HPBarItem)hpBarItemObject.Target;
|
||||
}
|
||||
else
|
||||
{
|
||||
hpBarItem = Instantiate(_hpBarItemTemplate);
|
||||
Transform transform = hpBarItem.GetComponent<Transform>();
|
||||
transform.SetParent(_hpBarInstanceRoot);
|
||||
transform.localScale = Vector3.one;
|
||||
_hpBarItemObjectPool.Register(HPBarItemObject.Create(hpBarItem), true);
|
||||
}
|
||||
|
||||
return hpBarItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class DropPoolRoller
|
||||
{
|
||||
private const float RarityCurveScalePhase = 30f;
|
||||
private static readonly RarityType[] OrderedRarities =
|
||||
{
|
||||
RarityType.White,
|
||||
RarityType.Green,
|
||||
RarityType.Blue,
|
||||
RarityType.Purple,
|
||||
RarityType.Red
|
||||
};
|
||||
|
||||
private readonly List<DROutGameDropPool> _eligibleRowBuffer = new();
|
||||
private readonly float[] _rarityWeightBuffer = new float[OrderedRarities.Length];
|
||||
private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
|
||||
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
|
||||
{
|
||||
_dropPoolTable = dropPoolTable;
|
||||
}
|
||||
|
||||
public bool TryRollRow(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
Random random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity)
|
||||
{
|
||||
selectedRow = null;
|
||||
selectedRarity = RarityType.None;
|
||||
|
||||
DROutGameDropPool[] allRows = _dropPoolTable.GetAllDataRows();
|
||||
if (allRows == null || allRows.Length <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CollectEligibleRows(allRows, displayPhaseIndex, themeType);
|
||||
if (_eligibleRowBuffer.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
selectedRarity = RollRarity(displayPhaseIndex, random);
|
||||
if (selectedRarity == RarityType.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int totalWeight = 0;
|
||||
DROutGameDropPool fallbackRow = null;
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int rowWeight = row.GetWeight(selectedRarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fallbackRow = row;
|
||||
totalWeight += rowWeight;
|
||||
}
|
||||
|
||||
if (totalWeight <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int randomWeight = random.Next(1, totalWeight + 1);
|
||||
int cumulativeWeight = 0;
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int rowWeight = row.GetWeight(selectedRarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cumulativeWeight += rowWeight;
|
||||
if (randomWeight <= cumulativeWeight)
|
||||
{
|
||||
selectedRow = row;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
selectedRow = fallbackRow;
|
||||
return selectedRow != null;
|
||||
}
|
||||
|
||||
private void CollectEligibleRows(
|
||||
DROutGameDropPool[] allRows,
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType)
|
||||
{
|
||||
_eligibleRowBuffer.Clear();
|
||||
|
||||
foreach (var row in allRows)
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (row.LevelThemeType != themeType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsEligibleAtPhase(row, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_eligibleRowBuffer.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private RarityType RollRarity(int displayPhaseIndex, Random random)
|
||||
{
|
||||
for (int i = 0; i < _rarityWeightBuffer.Length; i++)
|
||||
{
|
||||
_rarityWeightBuffer[i] = 0f;
|
||||
}
|
||||
|
||||
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
|
||||
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||
{
|
||||
RarityType rarity = OrderedRarities[rarityIndex];
|
||||
if (!IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int rowWeight = row.GetWeight(rarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float curveWeight = GetRarityCurveWeight(rarity, phaseT);
|
||||
if (curveWeight <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_rarityWeightBuffer[rarityIndex] += rowWeight * curveWeight;
|
||||
}
|
||||
}
|
||||
|
||||
float totalWeight = 0f;
|
||||
foreach (var weight in _rarityWeightBuffer)
|
||||
{
|
||||
totalWeight += Mathf.Max(0f, weight);
|
||||
}
|
||||
|
||||
if (totalWeight <= 0f)
|
||||
{
|
||||
return RarityType.None;
|
||||
}
|
||||
|
||||
float randomWeight = (float)(random.NextDouble() * totalWeight);
|
||||
float cumulativeWeight = 0f;
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
cumulativeWeight += Mathf.Max(0f, _rarityWeightBuffer[rarityIndex]);
|
||||
if (randomWeight <= cumulativeWeight)
|
||||
{
|
||||
return OrderedRarities[rarityIndex];
|
||||
}
|
||||
}
|
||||
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
if (_rarityWeightBuffer[rarityIndex] > 0f)
|
||||
{
|
||||
return OrderedRarities[rarityIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return RarityType.None;
|
||||
}
|
||||
|
||||
private static bool IsEligibleAtPhase(DROutGameDropPool row, int displayPhaseIndex)
|
||||
{
|
||||
for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||
{
|
||||
RarityType rarity = OrderedRarities[rarityIndex];
|
||||
if (IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsEligibleAtPhase(DROutGameDropPool row, RarityType rarity, int displayPhaseIndex)
|
||||
{
|
||||
if (row.GetWeight(rarity) <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int minPhase = row.GetMinPhase(rarity);
|
||||
int maxPhase = row.GetMaxPhase(rarity);
|
||||
return displayPhaseIndex >= minPhase && displayPhaseIndex <= maxPhase;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class InventoryGenerationComponent : GameFrameworkComponent
|
||||
{
|
||||
private readonly List<DRShopPrice> _shopPriceRows = new();
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
private IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
private IDataTable<DRMuzzleComp> _muzzleCompTable;
|
||||
private IDataTable<DRBearingComp> _bearingCompTable;
|
||||
private IDataTable<DRBaseComp> _baseCompTable;
|
||||
private ShopGoodsBuilder _shopGoodsBuilder;
|
||||
private DropPoolRoller _dropPoolRoller;
|
||||
private RewardCandidateBuilder _rewardCandidateBuilder;
|
||||
private OutGameDropItemBuilder _outGameDropItemBuilder;
|
||||
|
||||
public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1)
|
||||
{
|
||||
EnsureShopBuilder();
|
||||
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex);
|
||||
}
|
||||
|
||||
public EnemyDropResult ResolveEnemyDrop(
|
||||
in EnemyDropContext context,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextDropOrdinal)
|
||||
{
|
||||
DREnemy enemy = context.Enemy;
|
||||
if (enemy == null)
|
||||
{
|
||||
return EnemyDropResult.Empty;
|
||||
}
|
||||
|
||||
int coin = Mathf.Max(0, enemy.DropCoin);
|
||||
int gold = 0;
|
||||
float dropRate = enemy.DropPercent > 1f
|
||||
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
||||
: Mathf.Clamp01(enemy.DropPercent);
|
||||
|
||||
int dropOrdinal = nextDropOrdinal;
|
||||
nextDropOrdinal++;
|
||||
InventoryGenerationRandomContext randomContext =
|
||||
new(runSeed, sequenceIndex, InventoryTagSourceType.Drop, dropOrdinal);
|
||||
Random random = randomContext.CreateRandom();
|
||||
|
||||
if (enemy.DropGold > 0 && dropRate > 0f && random.NextDouble() <= dropRate)
|
||||
{
|
||||
gold = Mathf.Max(0, enemy.DropGold);
|
||||
}
|
||||
|
||||
TowerCompItemData lootItem = null;
|
||||
if (OutGameDropRuleService.ShouldRollOutGameItem(context.DisplayPhaseIndex, random) &&
|
||||
TryRollOutGameItem(
|
||||
context.DisplayPhaseIndex,
|
||||
context.ThemeType,
|
||||
randomContext,
|
||||
random,
|
||||
out TowerCompItemData droppedItem))
|
||||
{
|
||||
lootItem = droppedItem;
|
||||
}
|
||||
|
||||
return new EnemyDropResult(coin, gold, lootItem);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int candidateCount,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextRewardOrdinal)
|
||||
{
|
||||
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
|
||||
int rewardOrdinal = nextRewardOrdinal;
|
||||
IReadOnlyList<TowerCompItemData> candidates = rewardCandidateBuilder.BuildCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
candidateCount,
|
||||
CreateNextRewardRandomContext,
|
||||
BuildRewardCandidateItem);
|
||||
nextRewardOrdinal = rewardOrdinal;
|
||||
return candidates;
|
||||
|
||||
InventoryGenerationRandomContext CreateNextRewardRandomContext()
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
InventoryTagSourceType.Reward,
|
||||
rewardOrdinal++);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
|
||||
int count,
|
||||
RarityType rarity,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex)
|
||||
{
|
||||
return BuildEventRewardComponents(
|
||||
count,
|
||||
rarity,
|
||||
rarity,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
|
||||
int count,
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex)
|
||||
{
|
||||
EnsureComponentTables();
|
||||
|
||||
int resolvedCount = Mathf.Max(0, count);
|
||||
if (resolvedCount <= 0)
|
||||
{
|
||||
return System.Array.Empty<TowerCompItemData>();
|
||||
}
|
||||
|
||||
RarityType normalizedMinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity);
|
||||
RarityType normalizedMaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity);
|
||||
if (normalizedMinRarity > normalizedMaxRarity)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
$"Event reward rarity range is invalid: {normalizedMinRarity} > {normalizedMaxRarity}.");
|
||||
}
|
||||
|
||||
List<TowerCompItemData> result = new List<TowerCompItemData>(resolvedCount);
|
||||
for (int i = 0; i < resolvedCount; i++)
|
||||
{
|
||||
InventoryGenerationRandomContext randomContext = CreateEventRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex,
|
||||
i);
|
||||
Random random = randomContext.CreateRandom();
|
||||
result.Add(BuildRandomEventComponentItem(normalizedMinRarity, normalizedMaxRarity, randomContext, random));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EnsureShopTables()
|
||||
{
|
||||
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
EnsureComponentTables();
|
||||
|
||||
if (_shopPriceTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires ShopPrice data table.");
|
||||
}
|
||||
|
||||
if (_shopPriceRows.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DRShopPrice[] rows = _shopPriceTable.GetAllDataRows();
|
||||
if (rows == null || rows.Length <= 0)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires at least one shop price row.");
|
||||
}
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (row != null)
|
||||
{
|
||||
_shopPriceRows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (_shopPriceRows.Count <= 0)
|
||||
{
|
||||
throw new System.InvalidOperationException("InventoryGenerationComponent requires non-null shop price rows.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureShopBuilder()
|
||||
{
|
||||
EnsureShopTables();
|
||||
_shopGoodsBuilder ??= new ShopGoodsBuilder(
|
||||
_shopPriceRows,
|
||||
_muzzleCompTable,
|
||||
_bearingCompTable,
|
||||
_baseCompTable);
|
||||
}
|
||||
|
||||
private void EnsureDropTables()
|
||||
{
|
||||
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
|
||||
EnsureComponentTables();
|
||||
|
||||
if (_dropPoolTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires OutGameDropPool data table.");
|
||||
}
|
||||
}
|
||||
|
||||
private DropPoolRoller EnsureDropPoolRoller()
|
||||
{
|
||||
EnsureDropTables();
|
||||
_dropPoolRoller ??= new DropPoolRoller(_dropPoolTable);
|
||||
return _dropPoolRoller;
|
||||
}
|
||||
|
||||
private RewardCandidateBuilder EnsureRewardCandidateBuilder()
|
||||
{
|
||||
_rewardCandidateBuilder ??= new RewardCandidateBuilder(EnsureDropPoolRoller());
|
||||
return _rewardCandidateBuilder;
|
||||
}
|
||||
|
||||
private OutGameDropItemBuilder EnsureOutGameDropItemBuilder()
|
||||
{
|
||||
EnsureDropTables();
|
||||
_outGameDropItemBuilder ??= new OutGameDropItemBuilder(
|
||||
_muzzleCompTable,
|
||||
_bearingCompTable,
|
||||
_baseCompTable);
|
||||
return _outGameDropItemBuilder;
|
||||
}
|
||||
|
||||
private bool TryRollOutGameItem(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random,
|
||||
out TowerCompItemData droppedItem)
|
||||
{
|
||||
droppedItem = null;
|
||||
DropPoolRoller dropPoolRoller = EnsureDropPoolRoller();
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
if (!dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return EnsureOutGameDropItemBuilder().TryBuildItem(selectedRow, selectedRarity, randomContext, out droppedItem);
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRewardCandidateItem(
|
||||
DROutGameDropPool row,
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext)
|
||||
{
|
||||
if (!EnsureOutGameDropItemBuilder().TryBuildItem(row, rarity, randomContext, out TowerCompItemData droppedItem))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return droppedItem;
|
||||
}
|
||||
|
||||
private void EnsureComponentTables()
|
||||
{
|
||||
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
|
||||
if (_muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
}
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRandomEventComponentItem(
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
RarityType rarity = ResolveEventRewardRarity(minRarity, maxRarity, random);
|
||||
int slotRoll = random.Next(0, 3);
|
||||
return slotRoll switch
|
||||
{
|
||||
0 => BuildRandomEventMuzzleItem(rarity, randomContext, random),
|
||||
1 => BuildRandomEventBearingItem(rarity, randomContext, random),
|
||||
_ => BuildRandomEventBaseItem(rarity, randomContext, random)
|
||||
};
|
||||
}
|
||||
|
||||
private static RarityType ResolveEventRewardRarity(
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
Random random)
|
||||
{
|
||||
if (minRarity >= maxRarity)
|
||||
{
|
||||
return minRarity;
|
||||
}
|
||||
|
||||
int rarityValue = random.Next((int)minRarity, (int)maxRarity + 1);
|
||||
return (RarityType)rarityValue;
|
||||
}
|
||||
|
||||
private MuzzleCompItemData BuildRandomEventMuzzleItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
|
||||
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateMuzzle(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BearingCompItemData BuildRandomEventBearingItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
|
||||
DRBearingComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateBearing(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BaseCompItemData BuildRandomEventBaseItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
|
||||
DRBaseComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateBase(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private static InventoryGenerationRandomContext CreateEventRandomContext(
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex,
|
||||
int itemIndex)
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
InventoryTagSourceType.Event,
|
||||
BuildEventLocalOrdinal(eventId, optionIndex, effectIndex, itemIndex));
|
||||
}
|
||||
|
||||
private static int BuildEventLocalOrdinal(int eventId, int optionIndex, int effectIndex, int itemIndex)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int value = 17;
|
||||
value = value * 31 + eventId;
|
||||
value = value * 31 + optionIndex;
|
||||
value = value * 31 + effectIndex;
|
||||
value = value * 31 + itemIndex;
|
||||
return value & int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.Definition
|
||||
{
|
||||
public readonly struct InventoryGenerationRandomContext
|
||||
{
|
||||
public InventoryGenerationRandomContext(
|
||||
int runSeed,
|
||||
int nodeSequenceIndex,
|
||||
InventoryTagSourceType sourceType,
|
||||
int localOrdinal)
|
||||
{
|
||||
RunSeed = runSeed;
|
||||
NodeSequenceIndex = nodeSequenceIndex;
|
||||
SourceType = sourceType;
|
||||
LocalOrdinal = localOrdinal;
|
||||
}
|
||||
|
||||
public int RunSeed { get; }
|
||||
|
||||
public int NodeSequenceIndex { get; }
|
||||
|
||||
public InventoryTagSourceType SourceType { get; }
|
||||
|
||||
public int LocalOrdinal { get; }
|
||||
|
||||
public Random CreateRandom()
|
||||
{
|
||||
return new Random(BuildSeed());
|
||||
}
|
||||
|
||||
public long CreateStableItemInstanceId()
|
||||
{
|
||||
long normalizedSource = ((long)Math.Max(0, (int)SourceType) + 1L) << 48;
|
||||
long normalizedSequence = ((long)Math.Max(0, NodeSequenceIndex) + 1L) << 24;
|
||||
long normalizedOrdinal = (uint)(Math.Max(0, LocalOrdinal) + 1);
|
||||
return normalizedSource | normalizedSequence | normalizedOrdinal;
|
||||
}
|
||||
|
||||
public InventoryTagRandomContext CreateTagRandomContext(int configId)
|
||||
{
|
||||
return SourceType switch
|
||||
{
|
||||
InventoryTagSourceType.Shop => InventoryTagRandomContext.CreateShop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
InventoryTagSourceType.Event => InventoryTagRandomContext.CreateEvent(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
_ => InventoryTagRandomContext.CreateDrop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId)
|
||||
};
|
||||
}
|
||||
|
||||
private int BuildSeed()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + RunSeed;
|
||||
seed = seed * 31 + NodeSequenceIndex;
|
||||
seed = seed * 31 + (int)SourceType;
|
||||
seed = seed * 31 + LocalOrdinal;
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public static class OutGameDropRuleService
|
||||
{
|
||||
private const float DropChanceBase = 0.05f;
|
||||
private const float DropChancePerPhase = 0.2f;
|
||||
private const float DropChanceCap = 0.2f;
|
||||
|
||||
public static bool ShouldRollOutGameItem(int displayPhaseIndex, Random random)
|
||||
{
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
|
||||
return random.NextDouble() <= dropChance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class RewardCandidateBuilder
|
||||
{
|
||||
private readonly DropPoolRoller _dropPoolRoller;
|
||||
|
||||
public RewardCandidateBuilder(DropPoolRoller dropPoolRoller)
|
||||
{
|
||||
_dropPoolRoller = dropPoolRoller;
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildCandidates(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int candidateCount,
|
||||
Func<InventoryGenerationRandomContext> createRandomContext,
|
||||
Func<DROutGameDropPool, RarityType, InventoryGenerationRandomContext, TowerCompItemData> buildRewardItem)
|
||||
{
|
||||
int resolvedCount = Mathf.Max(0, candidateCount);
|
||||
if (resolvedCount <= 0)
|
||||
{
|
||||
return Array.Empty<TowerCompItemData>();
|
||||
}
|
||||
|
||||
List<TowerCompItemData> candidates = new List<TowerCompItemData>(resolvedCount);
|
||||
HashSet<long> selectedPoolEntryKeys = new HashSet<long>();
|
||||
int maxAttempts = Mathf.Max(resolvedCount * 6, resolvedCount);
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
|
||||
int attempts = 0;
|
||||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||
{
|
||||
attempts++;
|
||||
InventoryGenerationRandomContext randomContext = createRandomContext();
|
||||
Random random = randomContext.CreateRandom();
|
||||
if (!_dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!selectedPoolEntryKeys.Add(BuildSelectionKey(selectedRow.Id, selectedRarity)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
|
||||
if (candidate == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
attempts = 0;
|
||||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||
{
|
||||
attempts++;
|
||||
InventoryGenerationRandomContext randomContext = createRandomContext();
|
||||
Random random = randomContext.CreateRandom();
|
||||
if (!_dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
|
||||
if (candidate == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static long BuildSelectionKey(int rowId, RarityType rarity)
|
||||
{
|
||||
return ((long)rowId << 32) | (uint)rarity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class ShopGoodsBuilder
|
||||
{
|
||||
private readonly IReadOnlyList<DRShopPrice> _shopPriceRows;
|
||||
private readonly IDataTable<DRMuzzleComp> _muzzleCompTable;
|
||||
private readonly IDataTable<DRBearingComp> _bearingCompTable;
|
||||
private readonly IDataTable<DRBaseComp> _baseCompTable;
|
||||
|
||||
public ShopGoodsBuilder(
|
||||
IReadOnlyList<DRShopPrice> shopPriceRows,
|
||||
IDataTable<DRMuzzleComp> muzzleCompTable,
|
||||
IDataTable<DRBearingComp> bearingCompTable,
|
||||
IDataTable<DRBaseComp> baseCompTable)
|
||||
{
|
||||
_shopPriceRows = shopPriceRows;
|
||||
_muzzleCompTable = muzzleCompTable;
|
||||
_bearingCompTable = bearingCompTable;
|
||||
_baseCompTable = baseCompTable;
|
||||
}
|
||||
|
||||
public List<GoodsItemRawData> BuildGoods(
|
||||
int goodsCount,
|
||||
int runSeed,
|
||||
int sequenceIndex)
|
||||
{
|
||||
if (goodsCount <= 0)
|
||||
{
|
||||
return new List<GoodsItemRawData>();
|
||||
}
|
||||
|
||||
List<GoodsItemRawData> goodsItems = new(goodsCount);
|
||||
for (int i = 0; i < goodsCount; i++)
|
||||
{
|
||||
InventoryGenerationRandomContext randomContext =
|
||||
new(runSeed, sequenceIndex, InventoryTagSourceType.Shop, i);
|
||||
goodsItems.Add(BuildGoodsItem(i, randomContext));
|
||||
}
|
||||
|
||||
return goodsItems;
|
||||
}
|
||||
|
||||
private GoodsItemRawData BuildGoodsItem(
|
||||
int goodsIndex,
|
||||
InventoryGenerationRandomContext randomContext)
|
||||
{
|
||||
Random random = randomContext.CreateRandom();
|
||||
TowerCompItemData sourceItem = BuildRandomComponentItem(randomContext, random);
|
||||
return new GoodsItemRawData
|
||||
{
|
||||
GoodsIndex = goodsIndex,
|
||||
Title = sourceItem.Name,
|
||||
TypeText = BuildTypeText(sourceItem.SlotType),
|
||||
Description = BuildDescription(sourceItem),
|
||||
Price = ResolveRandomPrice(sourceItem.Rarity, random),
|
||||
Tags = sourceItem.Tags != null ? (TagType[])sourceItem.Tags.Clone() : Array.Empty<TagType>(),
|
||||
IconAreaContext = BuildIconAreaContext(sourceItem),
|
||||
SourceItem = sourceItem,
|
||||
IsPurchased = false
|
||||
};
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRandomComponentItem(
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
int slotRoll = random.Next(0, 3);
|
||||
DRShopPrice priceRow = _shopPriceRows[random.Next(0, _shopPriceRows.Count)];
|
||||
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(
|
||||
priceRow != null ? priceRow.Rarity : RarityType.White);
|
||||
|
||||
return slotRoll switch
|
||||
{
|
||||
0 => BuildRandomMuzzleItem(rarity, randomContext, random),
|
||||
1 => BuildRandomBearingItem(rarity, randomContext, random),
|
||||
_ => BuildRandomBaseItem(rarity, randomContext, random)
|
||||
};
|
||||
}
|
||||
|
||||
private MuzzleCompItemData BuildRandomMuzzleItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
|
||||
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BearingCompItemData BuildRandomBearingItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
|
||||
DRBearingComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BaseCompItemData BuildRandomBaseItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
|
||||
DRBaseComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private int ResolveRandomPrice(RarityType rarity, Random random)
|
||||
{
|
||||
return ShopPriceRuleService.ResolveRandomBuyPrice(_shopPriceRows, rarity, random);
|
||||
}
|
||||
|
||||
private static IconAreaContext BuildIconAreaContext(TowerCompItemData item)
|
||||
{
|
||||
return new IconAreaContext
|
||||
{
|
||||
Rarity = item.Rarity,
|
||||
ComponentSlotType = item.SlotType,
|
||||
Color = IconColorGenerator.GenerateForComponent(item)
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildTypeText(TowerCompSlotType slotType)
|
||||
{
|
||||
return slotType switch
|
||||
{
|
||||
TowerCompSlotType.Muzzle => "枪口组件",
|
||||
TowerCompSlotType.Bearing => "轴承组件",
|
||||
TowerCompSlotType.Base => "底座组件",
|
||||
_ => "组件"
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildDescription(TowerCompItemData item)
|
||||
{
|
||||
if (item is MuzzleCompItemData muzzleComp)
|
||||
{
|
||||
return ItemDescUtility.BuildMuzzleDesc(muzzleComp);
|
||||
}
|
||||
|
||||
if (item is BearingCompItemData bearingComp)
|
||||
{
|
||||
return ItemDescUtility.BuildBearingDesc(bearingComp);
|
||||
}
|
||||
|
||||
if (item is BaseCompItemData baseComp)
|
||||
{
|
||||
return ItemDescUtility.BuildBaseDesc(baseComp);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class PlayerInventoryComponent : GameFrameworkComponent
|
||||
{
|
||||
private const int MaxParticipantTowerCount = 4;
|
||||
|
||||
private PlayerInventoryState _state;
|
||||
private PlayerInventoryQueryModel _queryModel;
|
||||
private PlayerInventoryCommandModel _commandModel;
|
||||
private PlayerInventoryTowerRosterService _towerRosterService;
|
||||
private PlayerInventoryTowerAssemblyService _towerAssemblyService;
|
||||
private PlayerInventoryTradeService _tradeService;
|
||||
|
||||
public int Gold
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _queryModel.Gold;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnInit(BackpackInventoryData initialInventory = null)
|
||||
{
|
||||
EnsureServices();
|
||||
_commandModel.Initialize(initialInventory ?? InventorySeedUtility.CreateSampleInventory(), MaxParticipantTowerCount);
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
Log.Info(
|
||||
"PlayerInventory initialized. Gold={0}, Tower={1}, Muzzle={2}, Bearing={3}, Base={4}.",
|
||||
inventory.Gold,
|
||||
inventory.Towers.Count,
|
||||
inventory.MuzzleComponents.Count,
|
||||
inventory.BearingComponents.Count,
|
||||
inventory.BaseComponents.Count);
|
||||
}
|
||||
|
||||
public BackpackInventoryData GetInventorySnapshot()
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _queryModel.GetSnapshot();
|
||||
}
|
||||
|
||||
public void ReplaceInventorySnapshot(BackpackInventoryData inventorySnapshot)
|
||||
{
|
||||
EnsureServices();
|
||||
_commandModel.ReplaceInventorySnapshot(inventorySnapshot, MaxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerItemData> GetParticipantTowerSnapshot()
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (inventory?.ParticipantTowerInstanceIds == null || inventory.ParticipantTowerInstanceIds.Count <= 0)
|
||||
{
|
||||
return new List<TowerItemData>();
|
||||
}
|
||||
|
||||
List<TowerItemData> result = new List<TowerItemData>(inventory.ParticipantTowerInstanceIds.Count);
|
||||
for (int i = 0; i < inventory.ParticipantTowerInstanceIds.Count && result.Count < MaxParticipantTowerCount; i++)
|
||||
{
|
||||
long towerId = inventory.ParticipantTowerInstanceIds[i];
|
||||
if (!_queryModel.TryGetTowerById(towerId, out TowerItemData tower) || tower == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(InventoryCloneUtility.CloneTower(tower));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void MergeInventory(BackpackInventoryData gainedInventory)
|
||||
{
|
||||
EnsureInitialized();
|
||||
PlayerInventoryMergeSummary summary = _commandModel.MergeInventory(gainedInventory);
|
||||
if (!summary.HasAnyGain)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
"PlayerInventory merged reward. Gold+{0}, Tower+{1}, Muzzle+{2}, Bearing+{3}, Base+{4}.",
|
||||
summary.GainedGold,
|
||||
summary.GainedTowerCount,
|
||||
summary.GainedMuzzleCount,
|
||||
summary.GainedBearingCount,
|
||||
summary.GainedBaseCount);
|
||||
}
|
||||
|
||||
public bool TryConsumeGold(int costGold)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _commandModel.TryConsumeGold(costGold);
|
||||
}
|
||||
|
||||
public bool TryPurchaseComponent(TowerCompItemData item, int price)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TryPurchaseComponent(item, price);
|
||||
}
|
||||
|
||||
public void AddGold(int gainGold)
|
||||
{
|
||||
EnsureInitialized();
|
||||
_commandModel.AddGold(gainGold);
|
||||
}
|
||||
|
||||
public ParticipantTowerAssignResult TryAddParticipantTower(long towerInstanceId, int maxCount = 4)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerRosterService.TryAddParticipantTower(towerInstanceId, maxCount);
|
||||
}
|
||||
|
||||
public bool TryRemoveParticipantTower(long towerInstanceId)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerRosterService.TryRemoveParticipantTower(towerInstanceId);
|
||||
}
|
||||
|
||||
public bool TryAssembleTower(
|
||||
long muzzleInstanceId,
|
||||
long bearingInstanceId,
|
||||
long baseInstanceId,
|
||||
out TowerItemData assembledTower)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerAssemblyService.TryAssembleTower(
|
||||
muzzleInstanceId,
|
||||
bearingInstanceId,
|
||||
baseInstanceId,
|
||||
out assembledTower);
|
||||
}
|
||||
|
||||
public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerRosterService.ReduceTowerEndurance(towerInstanceIds, enduranceLoss);
|
||||
}
|
||||
|
||||
public bool TryDisassembleTower(long towerInstanceId)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerAssemblyService.TryDisassembleTower(towerInstanceId);
|
||||
}
|
||||
|
||||
public bool TryGetSaleCandidate(long itemId, out PlayerInventorySaleCandidate candidate)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TryGetSaleCandidate(itemId, out candidate);
|
||||
}
|
||||
|
||||
public bool TrySellItems(IReadOnlyCollection<long> itemIds, out PlayerInventorySaleResult result)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TrySellItems(itemIds, out result);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_queryModel.IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnInit();
|
||||
}
|
||||
|
||||
private void EnsureServices()
|
||||
{
|
||||
_state ??= new PlayerInventoryState();
|
||||
_queryModel ??= new PlayerInventoryQueryModel(_state);
|
||||
_commandModel ??= new PlayerInventoryCommandModel(_state);
|
||||
_towerRosterService ??= new PlayerInventoryTowerRosterService(_queryModel, MaxParticipantTowerCount);
|
||||
_towerAssemblyService ??= new PlayerInventoryTowerAssemblyService(_queryModel, _commandModel);
|
||||
_tradeService ??= new PlayerInventoryTradeService(_queryModel, _commandModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public struct PlayerInventoryMergeSummary
|
||||
{
|
||||
public int GainedGold;
|
||||
public int GainedMuzzleCount;
|
||||
public int GainedBearingCount;
|
||||
public int GainedBaseCount;
|
||||
public int GainedTowerCount;
|
||||
|
||||
public bool HasAnyGain =>
|
||||
GainedGold > 0 || GainedMuzzleCount > 0 || GainedBearingCount > 0 || GainedBaseCount > 0 ||
|
||||
GainedTowerCount > 0;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryState
|
||||
{
|
||||
public BackpackInventoryData Inventory = new BackpackInventoryData();
|
||||
public long NextInstanceId = 1;
|
||||
public bool IsInitialized;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryQueryModel
|
||||
{
|
||||
private readonly PlayerInventoryState _state;
|
||||
|
||||
public PlayerInventoryQueryModel(PlayerInventoryState state)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public BackpackInventoryData Inventory => _state.Inventory;
|
||||
public bool IsInitialized => _state.IsInitialized;
|
||||
public int Gold => _state.Inventory.Gold;
|
||||
|
||||
public BackpackInventoryData GetSnapshot()
|
||||
{
|
||||
return InventoryCloneUtility.CloneInventory(_state.Inventory);
|
||||
}
|
||||
|
||||
public bool TryGetTowerById(long towerInstanceId, out TowerItemData tower)
|
||||
{
|
||||
return InventoryParticipantUtility.TryGetTowerById(_state.Inventory, towerInstanceId, out tower);
|
||||
}
|
||||
|
||||
public bool TryGetComponentById<TComp>(List<TComp> components, long instanceId, out TComp result)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
result = null;
|
||||
if (components == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComp component = components[i];
|
||||
if (component != null && component.InstanceId == instanceId)
|
||||
{
|
||||
result = component;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryCommandModel
|
||||
{
|
||||
private readonly PlayerInventoryState _state;
|
||||
|
||||
public PlayerInventoryCommandModel(PlayerInventoryState state)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public void Initialize(BackpackInventoryData sourceInventory, int maxParticipantTowerCount)
|
||||
{
|
||||
_state.Inventory = InventoryCloneUtility.CloneInventory(sourceInventory);
|
||||
InventoryParticipantUtility.NormalizeParticipantState(_state.Inventory, maxParticipantTowerCount);
|
||||
RebuildNextInstanceId();
|
||||
_state.IsInitialized = true;
|
||||
}
|
||||
|
||||
public void ReplaceInventorySnapshot(BackpackInventoryData sourceInventory, int maxParticipantTowerCount)
|
||||
{
|
||||
Initialize(sourceInventory, maxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public PlayerInventoryMergeSummary MergeInventory(BackpackInventoryData gainedInventory)
|
||||
{
|
||||
PlayerInventoryMergeSummary summary = default;
|
||||
if (gainedInventory == null)
|
||||
{
|
||||
return summary;
|
||||
}
|
||||
|
||||
summary.GainedGold = Mathf.Max(0, gainedInventory.Gold);
|
||||
if (summary.GainedGold > 0)
|
||||
{
|
||||
_state.Inventory.Gold += summary.GainedGold;
|
||||
}
|
||||
|
||||
if (gainedInventory.MuzzleComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.MuzzleComponents.Count; i++)
|
||||
{
|
||||
MuzzleCompItemData source = gainedInventory.MuzzleComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MuzzleCompItemData cloned = InventoryCloneUtility.CloneMuzzleComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_state.Inventory.MuzzleComponents.Add(cloned);
|
||||
summary.GainedMuzzleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.BearingComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.BearingComponents.Count; i++)
|
||||
{
|
||||
BearingCompItemData source = gainedInventory.BearingComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BearingCompItemData cloned = InventoryCloneUtility.CloneBearingComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_state.Inventory.BearingComponents.Add(cloned);
|
||||
summary.GainedBearingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.BaseComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.BaseComponents.Count; i++)
|
||||
{
|
||||
BaseCompItemData source = gainedInventory.BaseComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BaseCompItemData cloned = InventoryCloneUtility.CloneBaseComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_state.Inventory.BaseComponents.Add(cloned);
|
||||
summary.GainedBaseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.Towers != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData source = gainedInventory.Towers[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerItemData cloned = InventoryCloneUtility.CloneTower(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_state.Inventory.Towers.Add(cloned);
|
||||
summary.GainedTowerCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
public bool TryConsumeGold(int costGold)
|
||||
{
|
||||
int resolvedCost = Mathf.Max(0, costGold);
|
||||
if (resolvedCost <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_state.Inventory.Gold < resolvedCost)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_state.Inventory.Gold -= resolvedCost;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddGold(int gainGold)
|
||||
{
|
||||
int resolvedGain = Mathf.Max(0, gainGold);
|
||||
if (resolvedGain <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_state.Inventory.Gold += resolvedGain;
|
||||
}
|
||||
|
||||
public long AllocateInstanceId()
|
||||
{
|
||||
if (_state.NextInstanceId < 1)
|
||||
{
|
||||
_state.NextInstanceId = 1;
|
||||
}
|
||||
|
||||
return _state.NextInstanceId++;
|
||||
}
|
||||
|
||||
private void RebuildNextInstanceId()
|
||||
{
|
||||
long maxInstanceId = 0;
|
||||
BackpackInventoryData inventory = _state.Inventory;
|
||||
if (inventory.Towers != null)
|
||||
{
|
||||
for (int i = 0; i < inventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData item = inventory.Towers[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.MuzzleComponents != null)
|
||||
{
|
||||
for (int i = 0; i < inventory.MuzzleComponents.Count; i++)
|
||||
{
|
||||
MuzzleCompItemData item = inventory.MuzzleComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.BearingComponents != null)
|
||||
{
|
||||
for (int i = 0; i < inventory.BearingComponents.Count; i++)
|
||||
{
|
||||
BearingCompItemData item = inventory.BearingComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.BaseComponents != null)
|
||||
{
|
||||
for (int i = 0; i < inventory.BaseComponents.Count; i++)
|
||||
{
|
||||
BaseCompItemData item = inventory.BaseComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_state.NextInstanceId = Math.Max(1, maxInstanceId + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class PlayerInventoryTowerAssemblyService
|
||||
{
|
||||
private const int TowerLevelCount = 5;
|
||||
|
||||
private readonly PlayerInventoryQueryModel _queryModel;
|
||||
private readonly PlayerInventoryCommandModel _commandModel;
|
||||
private IDataTable<DRMuzzleComp> _drMuzzleComp;
|
||||
private IDataTable<DRBearingComp> _drBearingComp;
|
||||
private IDataTable<DRBaseComp> _drBaseComp;
|
||||
|
||||
public PlayerInventoryTowerAssemblyService(
|
||||
PlayerInventoryQueryModel queryModel,
|
||||
PlayerInventoryCommandModel commandModel)
|
||||
{
|
||||
_queryModel = queryModel;
|
||||
_commandModel = commandModel;
|
||||
}
|
||||
|
||||
public bool TryAssembleTower(
|
||||
long muzzleInstanceId,
|
||||
long bearingInstanceId,
|
||||
long baseInstanceId,
|
||||
out TowerItemData assembledTower)
|
||||
{
|
||||
assembledTower = null;
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (muzzleInstanceId <= 0 || bearingInstanceId <= 0 || baseInstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_queryModel.TryGetComponentById(inventory.MuzzleComponents, muzzleInstanceId, out MuzzleCompItemData muzzleComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BearingComponents, bearingInstanceId, out BearingCompItemData bearingComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BaseComponents, baseInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (muzzleComp.IsAssembledIntoTower || bearingComp.IsAssembledIntoTower || baseComp.IsAssembledIntoTower)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryBuildTowerStats(muzzleComp, bearingComp, baseComp, out TowerStatsData stats))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long towerInstanceId = _commandModel.AllocateInstanceId();
|
||||
TowerItemData tower = new TowerItemData
|
||||
{
|
||||
InstanceId = towerInstanceId,
|
||||
Name = $"组装防御塔-{towerInstanceId}",
|
||||
Rarity = InventoryRarityRuleService.ResolveTowerRarity(
|
||||
muzzleComp.Rarity,
|
||||
bearingComp.Rarity,
|
||||
baseComp.Rarity),
|
||||
MuzzleComponentInstanceId = muzzleComp.InstanceId,
|
||||
BearingComponentInstanceId = bearingComp.InstanceId,
|
||||
BaseComponentInstanceId = baseComp.InstanceId,
|
||||
Stats = stats
|
||||
};
|
||||
|
||||
muzzleComp.IsAssembledIntoTower = true;
|
||||
bearingComp.IsAssembledIntoTower = true;
|
||||
baseComp.IsAssembledIntoTower = true;
|
||||
inventory.Towers.Add(tower);
|
||||
assembledTower = InventoryCloneUtility.CloneTower(tower);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryBuildTowerStats(
|
||||
MuzzleCompItemData muzzleComp,
|
||||
BearingCompItemData bearingComp,
|
||||
BaseCompItemData baseComp,
|
||||
out TowerStatsData stats)
|
||||
{
|
||||
stats = null;
|
||||
if (muzzleComp == null || bearingComp == null || baseComp == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DRMuzzleComp muzzleConfig = EnsureMuzzleTable()?.GetDataRow(muzzleComp.ConfigId);
|
||||
DRBearingComp bearingConfig = EnsureBearingTable()?.GetDataRow(bearingComp.ConfigId);
|
||||
DRBaseComp baseConfig = EnsureBaseTable()?.GetDataRow(baseComp.ConfigId);
|
||||
if (muzzleConfig == null || bearingConfig == null || baseConfig == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stats = new TowerStatsData
|
||||
{
|
||||
AttackDamage = BuildLevelIntArray(muzzleComp.AttackDamage, muzzleComp.Rarity, muzzleConfig.AttackDamagePerLevel),
|
||||
DamageRandomRate = Mathf.Max(0f, muzzleComp.DamageRandomRate),
|
||||
RotateSpeed = BuildLevelFloatArray(bearingComp.RotateSpeed, bearingComp.Rarity, bearingConfig.RotateSpeedPerLevel),
|
||||
AttackRange = BuildLevelFloatArray(bearingComp.AttackRange, bearingComp.Rarity, bearingConfig.AttackRangePerLevel),
|
||||
AttackSpeed = BuildLevelFloatArray(baseComp.AttackSpeed, baseComp.Rarity, baseConfig.AttackSpeedPerLevel),
|
||||
AttackMethodType = muzzleComp.AttackMethodType,
|
||||
AttackPropertyType = baseComp.AttackPropertyType,
|
||||
TagRuntimes = TowerTagAggregationService.AggregateTowerTags(
|
||||
muzzleComp.Tags,
|
||||
bearingComp.Tags,
|
||||
baseComp.Tags)
|
||||
};
|
||||
stats.Tags = TowerTagAggregationService.FlattenUniqueTags(stats.TagRuntimes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int[] BuildLevelIntArray(int[] rarityBaseArray, RarityType rarity, int perLevel)
|
||||
{
|
||||
int baseValue = ResolveRarityBaseValue(rarityBaseArray, rarity);
|
||||
int[] values = new int[TowerLevelCount];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = Mathf.Max(0, baseValue + perLevel * i);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static float[] BuildLevelFloatArray(float[] rarityBaseArray, RarityType rarity, float perLevel)
|
||||
{
|
||||
float baseValue = ResolveRarityBaseValue(rarityBaseArray, rarity);
|
||||
float[] values = new float[TowerLevelCount];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = Mathf.Max(0f, baseValue + perLevel * i);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static int ResolveRarityBaseValue(int[] rarityBaseArray, RarityType rarity)
|
||||
{
|
||||
if (rarityBaseArray == null || rarityBaseArray.Length <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rarityIndex = Mathf.Clamp((int)rarity - 1, 0, rarityBaseArray.Length - 1);
|
||||
return rarityBaseArray[rarityIndex];
|
||||
}
|
||||
|
||||
private static float ResolveRarityBaseValue(float[] rarityBaseArray, RarityType rarity)
|
||||
{
|
||||
if (rarityBaseArray == null || rarityBaseArray.Length <= 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
int rarityIndex = Mathf.Clamp((int)rarity - 1, 0, rarityBaseArray.Length - 1);
|
||||
return rarityBaseArray[rarityIndex];
|
||||
}
|
||||
|
||||
private IDataTable<DRMuzzleComp> EnsureMuzzleTable()
|
||||
{
|
||||
_drMuzzleComp ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
return _drMuzzleComp;
|
||||
}
|
||||
|
||||
private IDataTable<DRBearingComp> EnsureBearingTable()
|
||||
{
|
||||
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
return _drBearingComp;
|
||||
}
|
||||
|
||||
private IDataTable<DRBaseComp> EnsureBaseTable()
|
||||
{
|
||||
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
return _drBaseComp;
|
||||
}
|
||||
|
||||
public bool TryDisassembleTower(long towerInstanceId)
|
||||
{
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (towerInstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_queryModel.TryGetTowerById(towerInstanceId, out TowerItemData tower) || tower == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_queryModel.TryGetComponentById(inventory.MuzzleComponents, tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BearingComponents, tower.BearingComponentInstanceId, out BearingCompItemData bearingComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BaseComponents, tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
InventoryParticipantUtility.TryRemoveParticipantTower(inventory, towerInstanceId, MaxParticipantTowerCount);
|
||||
|
||||
muzzleComp.IsAssembledIntoTower = false;
|
||||
bearingComp.IsAssembledIntoTower = false;
|
||||
baseComp.IsAssembledIntoTower = false;
|
||||
|
||||
inventory.Towers.Remove(tower);
|
||||
return true;
|
||||
}
|
||||
|
||||
private const int MaxParticipantTowerCount = 4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class PlayerInventoryTowerRosterService
|
||||
{
|
||||
private readonly PlayerInventoryQueryModel _queryModel;
|
||||
private readonly int _maxParticipantTowerCount;
|
||||
|
||||
public PlayerInventoryTowerRosterService(PlayerInventoryQueryModel queryModel, int maxParticipantTowerCount)
|
||||
{
|
||||
_queryModel = queryModel;
|
||||
_maxParticipantTowerCount = Mathf.Max(1, maxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public ParticipantTowerAssignResult TryAddParticipantTower(long towerInstanceId, int maxCount)
|
||||
{
|
||||
int resolvedMaxCount = Mathf.Max(1, maxCount);
|
||||
resolvedMaxCount = Mathf.Min(resolvedMaxCount, _maxParticipantTowerCount);
|
||||
return InventoryParticipantUtility.TryAddParticipantTower(
|
||||
_queryModel.Inventory,
|
||||
towerInstanceId,
|
||||
resolvedMaxCount);
|
||||
}
|
||||
|
||||
public bool TryRemoveParticipantTower(long towerInstanceId)
|
||||
{
|
||||
return InventoryParticipantUtility.TryRemoveParticipantTower(
|
||||
_queryModel.Inventory,
|
||||
towerInstanceId,
|
||||
_maxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss)
|
||||
{
|
||||
return InventoryTowerEnduranceUtility.ReduceTowerEndurance(
|
||||
_queryModel.Inventory,
|
||||
towerInstanceIds,
|
||||
enduranceLoss);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InventoryTowerEnduranceUtility
|
||||
{
|
||||
public static int ReduceTowerEndurance(
|
||||
BackpackInventoryData inventory,
|
||||
IReadOnlyList<long> towerInstanceIds,
|
||||
float enduranceLoss)
|
||||
{
|
||||
float resolvedLoss = Mathf.Max(0f, enduranceLoss);
|
||||
if (inventory?.Towers == null ||
|
||||
inventory.Towers.Count <= 0 ||
|
||||
resolvedLoss <= 0f ||
|
||||
towerInstanceIds == null ||
|
||||
towerInstanceIds.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Dictionary<long, MuzzleCompItemData> muzzleMap = BuildComponentMap(inventory.MuzzleComponents);
|
||||
Dictionary<long, BearingCompItemData> bearingMap = BuildComponentMap(inventory.BearingComponents);
|
||||
Dictionary<long, BaseCompItemData> baseMap = BuildComponentMap(inventory.BaseComponents);
|
||||
HashSet<long> processedTowerIds = new HashSet<long>();
|
||||
|
||||
int affectedCount = 0;
|
||||
for (int i = 0; i < towerInstanceIds.Count; i++)
|
||||
{
|
||||
long towerInstanceId = towerInstanceIds[i];
|
||||
if (towerInstanceId <= 0 || !processedTowerIds.Add(towerInstanceId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!InventoryParticipantUtility.TryGetTowerById(inventory, towerInstanceId, out TowerItemData tower) ||
|
||||
tower == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool towerAffected = false;
|
||||
if (muzzleMap.TryGetValue(tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp))
|
||||
{
|
||||
towerAffected |= TryReduceComponentEndurance(muzzleComp, resolvedLoss);
|
||||
}
|
||||
|
||||
if (bearingMap.TryGetValue(tower.BearingComponentInstanceId, out BearingCompItemData bearingComp))
|
||||
{
|
||||
towerAffected |= TryReduceComponentEndurance(bearingComp, resolvedLoss);
|
||||
}
|
||||
|
||||
if (baseMap.TryGetValue(tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
towerAffected |= TryReduceComponentEndurance(baseComp, resolvedLoss);
|
||||
}
|
||||
|
||||
if (towerAffected)
|
||||
{
|
||||
affectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return affectedCount;
|
||||
}
|
||||
|
||||
private static bool TryReduceComponentEndurance(TowerCompItemData component, float enduranceLoss)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float originalEndurance = component.Endurance;
|
||||
float nextEndurance = Mathf.Clamp(originalEndurance - Mathf.Max(0f, enduranceLoss), 0f, 100f);
|
||||
if (nextEndurance >= originalEndurance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
component.Endurance = nextEndurance;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Dictionary<long, TComp> BuildComponentMap<TComp>(List<TComp> components)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
Dictionary<long, TComp> map = new Dictionary<long, TComp>();
|
||||
if (components == null || components.Count <= 0)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComp component = components[i];
|
||||
if (component == null || component.InstanceId <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
map[component.InstanceId] = component;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public enum PlayerInventorySaleFailureReason : byte
|
||||
{
|
||||
None = 0,
|
||||
InvalidSelection = 1,
|
||||
ItemNotFound = 2,
|
||||
AssembledComponent = 3,
|
||||
ParticipantTower = 4,
|
||||
MissingTowerComponent = 5
|
||||
}
|
||||
|
||||
public sealed class PlayerInventorySaleCandidate
|
||||
{
|
||||
public long ItemId;
|
||||
public bool IsSellable;
|
||||
public bool IsTower;
|
||||
public int Price;
|
||||
public PlayerInventorySaleFailureReason FailureReason;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventorySaleResult
|
||||
{
|
||||
public int GainedGold;
|
||||
public int SoldComponentCount;
|
||||
public int SoldTowerCount;
|
||||
public PlayerInventorySaleFailureReason FailureReason;
|
||||
|
||||
public bool IsSuccess => FailureReason == PlayerInventorySaleFailureReason.None;
|
||||
public int SoldItemCount => SoldComponentCount + SoldTowerCount;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryTradeService
|
||||
{
|
||||
private readonly PlayerInventoryQueryModel _queryModel;
|
||||
private readonly PlayerInventoryCommandModel _commandModel;
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
|
||||
public PlayerInventoryTradeService(
|
||||
PlayerInventoryQueryModel queryModel,
|
||||
PlayerInventoryCommandModel commandModel,
|
||||
IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
_queryModel = queryModel;
|
||||
_commandModel = commandModel;
|
||||
_shopPriceTable = shopPriceTable;
|
||||
}
|
||||
|
||||
public bool TryPurchaseComponent(TowerCompItemData item, int price)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_commandModel.TryConsumeGold(price))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventoryDelta = WrapSingleItem(item);
|
||||
PlayerInventoryMergeSummary summary = _commandModel.MergeInventory(inventoryDelta);
|
||||
return summary.HasAnyGain;
|
||||
}
|
||||
|
||||
public bool TryGetSaleCandidate(long itemId, out PlayerInventorySaleCandidate candidate)
|
||||
{
|
||||
candidate = BuildSaleCandidate(itemId);
|
||||
return candidate != null;
|
||||
}
|
||||
|
||||
public bool TrySellItems(IReadOnlyCollection<long> itemIds, out PlayerInventorySaleResult result)
|
||||
{
|
||||
result = new PlayerInventorySaleResult();
|
||||
if (itemIds == null || itemIds.Count <= 0)
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
HashSet<long> uniqueIds = new HashSet<long>();
|
||||
List<PlayerInventorySaleCandidate> candidates = new List<PlayerInventorySaleCandidate>(itemIds.Count);
|
||||
foreach (long itemId in itemIds)
|
||||
{
|
||||
if (itemId <= 0 || !uniqueIds.Add(itemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerInventorySaleCandidate candidate = BuildSaleCandidate(itemId);
|
||||
if (candidate == null || !candidate.IsSellable)
|
||||
{
|
||||
result.FailureReason = candidate?.FailureReason ?? PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
if (candidates.Count <= 0)
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
PlayerInventorySaleCandidate candidate = candidates[i];
|
||||
if (candidate.IsTower)
|
||||
{
|
||||
if (!TryRemoveTower(inventory, candidate.ItemId))
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.MissingTowerComponent;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.SoldTowerCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryRemoveComponent(inventory, candidate.ItemId))
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.SoldComponentCount++;
|
||||
}
|
||||
|
||||
result.GainedGold += Mathf.Max(0, candidate.Price);
|
||||
}
|
||||
|
||||
_commandModel.AddGold(result.GainedGold);
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
private PlayerInventorySaleCandidate BuildSaleCandidate(long itemId)
|
||||
{
|
||||
if (itemId <= 0)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.InvalidSelection
|
||||
};
|
||||
}
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (inventory == null)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (_queryModel.TryGetTowerById(itemId, out TowerItemData tower) && tower != null)
|
||||
{
|
||||
if (inventory.ParticipantTowerInstanceIds != null && inventory.ParticipantTowerInstanceIds.Contains(itemId))
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
IsTower = true,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ParticipantTower
|
||||
};
|
||||
}
|
||||
|
||||
if (!ShopPriceRuleService.TryResolveTowerSalePrice(tower, inventory, out int towerPrice, EnsureShopPriceTable()))
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
IsTower = true,
|
||||
FailureReason = PlayerInventorySaleFailureReason.MissingTowerComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = true,
|
||||
IsTower = true,
|
||||
Price = towerPrice,
|
||||
FailureReason = PlayerInventorySaleFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.MuzzleComponents, itemId, out MuzzleCompItemData muzzleComp))
|
||||
{
|
||||
return BuildComponentCandidate(muzzleComp);
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.BearingComponents, itemId, out BearingCompItemData bearingComp))
|
||||
{
|
||||
return BuildComponentCandidate(bearingComp);
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.BaseComponents, itemId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return BuildComponentCandidate(baseComp);
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
private PlayerInventorySaleCandidate BuildComponentCandidate(TowerCompItemData component)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (component.IsAssembledIntoTower)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = component.InstanceId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.AssembledComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = component.InstanceId,
|
||||
IsSellable = true,
|
||||
IsTower = false,
|
||||
Price = ShopPriceRuleService.ResolveComponentSalePrice(component, EnsureShopPriceTable()),
|
||||
FailureReason = PlayerInventorySaleFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
private bool TryRemoveTower(BackpackInventoryData inventory, long towerId)
|
||||
{
|
||||
if (inventory?.Towers == null || towerId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TowerItemData targetTower = null;
|
||||
for (int i = 0; i < inventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData tower = inventory.Towers[i];
|
||||
if (tower != null && tower.InstanceId == towerId)
|
||||
{
|
||||
targetTower = tower;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTower == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ContainsInstanceId(inventory.MuzzleComponents, targetTower.MuzzleComponentInstanceId) ||
|
||||
!ContainsInstanceId(inventory.BearingComponents, targetTower.BearingComponentInstanceId) ||
|
||||
!ContainsInstanceId(inventory.BaseComponents, targetTower.BaseComponentInstanceId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool removedMuzzle = RemoveByInstanceId(inventory.MuzzleComponents, targetTower.MuzzleComponentInstanceId);
|
||||
bool removedBearing = RemoveByInstanceId(inventory.BearingComponents, targetTower.BearingComponentInstanceId);
|
||||
bool removedBase = RemoveByInstanceId(inventory.BaseComponents, targetTower.BaseComponentInstanceId);
|
||||
if (!removedMuzzle || !removedBearing || !removedBase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
inventory.Towers.Remove(targetTower);
|
||||
inventory.ParticipantTowerInstanceIds?.Remove(towerId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryRemoveComponent(BackpackInventoryData inventory, long itemId)
|
||||
{
|
||||
return RemoveByInstanceId(inventory?.MuzzleComponents, itemId) ||
|
||||
RemoveByInstanceId(inventory?.BearingComponents, itemId) ||
|
||||
RemoveByInstanceId(inventory?.BaseComponents, itemId);
|
||||
}
|
||||
|
||||
private static bool RemoveByInstanceId<TItem>(List<TItem> items, long instanceId)
|
||||
where TItem : class
|
||||
{
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
switch (items[i])
|
||||
{
|
||||
case TowerCompItemData component when component.InstanceId == instanceId:
|
||||
items.RemoveAt(i);
|
||||
return true;
|
||||
case TowerItemData tower when tower.InstanceId == instanceId:
|
||||
items.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsInstanceId<TItem>(IReadOnlyList<TItem> items, long instanceId)
|
||||
where TItem : class
|
||||
{
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
switch (items[i])
|
||||
{
|
||||
case TowerCompItemData component when component.InstanceId == instanceId:
|
||||
return true;
|
||||
case TowerItemData tower when tower.InstanceId == instanceId:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetComponentById<TComp>(IReadOnlyList<TComp> items, long instanceId, out TComp result)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
result = null;
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
TComp item = items[i];
|
||||
if (item != null && item.InstanceId == instanceId)
|
||||
{
|
||||
result = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static BackpackInventoryData WrapSingleItem(TowerCompItemData item)
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData();
|
||||
switch (item)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
inventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
inventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
inventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private IDataTable<DRShopPrice> EnsureShopPriceTable()
|
||||
{
|
||||
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
return _shopPriceTable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.Event;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps gameplay camera and UI in a fixed design aspect, then fills extra area with black masks.
|
||||
/// </summary>
|
||||
public class ResolutionAdapterComponent : GameFrameworkComponent
|
||||
{
|
||||
private const float DefaultCanvasPlaneDistance = 100f;
|
||||
|
||||
[SerializeField] private Vector2 _referenceResolution = new Vector2(2560f, 1600f);
|
||||
[SerializeField] private bool _adaptUiCanvasToViewport = true;
|
||||
[SerializeField] private bool _enableBlackMask = true;
|
||||
[SerializeField] private List<Transform> _uiRoots = new List<Transform>();
|
||||
|
||||
private readonly List<Canvas> _canvasBuffer = new List<Canvas>(32);
|
||||
private readonly List<Canvas> _trackedCanvases = new List<Canvas>(32);
|
||||
private readonly HashSet<Canvas> _trackedCanvasSet = new HashSet<Canvas>();
|
||||
|
||||
private Camera _mainCamera;
|
||||
private Rect _targetViewport = new Rect(0f, 0f, 1f, 1f);
|
||||
private int _cachedScreenWidth = -1;
|
||||
private int _cachedScreenHeight = -1;
|
||||
|
||||
private bool _canvasCacheDirty = true;
|
||||
private bool _uiEventSubscribed;
|
||||
private bool _missingRootWarned;
|
||||
|
||||
private Canvas _maskCanvas;
|
||||
private RectTransform _leftMask;
|
||||
private RectTransform _rightMask;
|
||||
private RectTransform _topMask;
|
||||
private RectTransform _bottomMask;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
TryEnsureUiEventSubscribed();
|
||||
ApplyAdaptation(true);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
TryEnsureUiEventSubscribed();
|
||||
ApplyAdaptation(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
UnsubscribeUiEvents();
|
||||
|
||||
if (_mainCamera != null)
|
||||
{
|
||||
_mainCamera.rect = new Rect(0f, 0f, 1f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyAdaptation(bool force)
|
||||
{
|
||||
if (_referenceResolution.x <= 0f || _referenceResolution.y <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Camera resolvedCamera = ResolveMainCamera();
|
||||
bool cameraChanged = resolvedCamera != _mainCamera;
|
||||
bool screenChanged = Screen.width != _cachedScreenWidth || Screen.height != _cachedScreenHeight;
|
||||
bool needRecalculate = force || cameraChanged || screenChanged;
|
||||
|
||||
if (needRecalculate)
|
||||
{
|
||||
Camera lastCamera = _mainCamera;
|
||||
if (lastCamera != null && lastCamera != resolvedCamera)
|
||||
{
|
||||
lastCamera.rect = new Rect(0f, 0f, 1f, 1f);
|
||||
}
|
||||
|
||||
_mainCamera = resolvedCamera;
|
||||
_cachedScreenWidth = Screen.width;
|
||||
_cachedScreenHeight = Screen.height;
|
||||
_targetViewport = CalculateViewport(_cachedScreenWidth, _cachedScreenHeight);
|
||||
|
||||
ApplyCameraViewport();
|
||||
UpdateMaskLayout();
|
||||
}
|
||||
|
||||
if (!_adaptUiCanvasToViewport)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!needRecalculate && !_canvasCacheDirty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyCanvasAdaptation();
|
||||
}
|
||||
|
||||
private Camera ResolveMainCamera()
|
||||
{
|
||||
if (GameEntry.Scene != null && GameEntry.Scene.MainCamera != null)
|
||||
{
|
||||
return GameEntry.Scene.MainCamera;
|
||||
}
|
||||
|
||||
if (Camera.main != null)
|
||||
{
|
||||
return Camera.main;
|
||||
}
|
||||
|
||||
if (_mainCamera != null && _mainCamera.isActiveAndEnabled)
|
||||
{
|
||||
return _mainCamera;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Rect CalculateViewport(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return new Rect(0f, 0f, 1f, 1f);
|
||||
}
|
||||
|
||||
float referenceAspect = _referenceResolution.x / _referenceResolution.y;
|
||||
float screenAspect = (float)width / height;
|
||||
|
||||
if (Mathf.Approximately(referenceAspect, screenAspect))
|
||||
{
|
||||
return new Rect(0f, 0f, 1f, 1f);
|
||||
}
|
||||
|
||||
if (screenAspect > referenceAspect)
|
||||
{
|
||||
float viewportWidth = referenceAspect / screenAspect;
|
||||
return new Rect((1f - viewportWidth) * 0.5f, 0f, viewportWidth, 1f);
|
||||
}
|
||||
|
||||
float viewportHeight = screenAspect / referenceAspect;
|
||||
return new Rect(0f, (1f - viewportHeight) * 0.5f, 1f, viewportHeight);
|
||||
}
|
||||
|
||||
private void ApplyCameraViewport()
|
||||
{
|
||||
if (_mainCamera == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_mainCamera.rect = _targetViewport;
|
||||
}
|
||||
|
||||
private void ApplyCanvasAdaptation()
|
||||
{
|
||||
if (_mainCamera == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_uiRoots == null || _uiRoots.Count == 0)
|
||||
{
|
||||
if (!_missingRootWarned)
|
||||
{
|
||||
_missingRootWarned = true;
|
||||
Log.Warning(
|
||||
"ResolutionAdapterComponent missing injected roots. Assign UI/HPBar root transforms in scene.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_canvasCacheDirty)
|
||||
{
|
||||
RebuildCanvasCache();
|
||||
}
|
||||
|
||||
for (int i = _trackedCanvases.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Canvas canvas = _trackedCanvases[i];
|
||||
if (canvas == null)
|
||||
{
|
||||
_trackedCanvases.RemoveAt(i);
|
||||
_canvasCacheDirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyCanvasSettings(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildCanvasCache()
|
||||
{
|
||||
_canvasCacheDirty = false;
|
||||
_trackedCanvases.Clear();
|
||||
_trackedCanvasSet.Clear();
|
||||
|
||||
foreach (var root in _uiRoots)
|
||||
{
|
||||
CollectCanvases(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void CollectCanvases(Transform root)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
root.GetComponentsInChildren(true, _canvasBuffer);
|
||||
foreach (Canvas canvas in _canvasBuffer)
|
||||
{
|
||||
if (canvas == null || canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_trackedCanvasSet.Add(canvas))
|
||||
{
|
||||
_trackedCanvases.Add(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
_canvasBuffer.Clear();
|
||||
}
|
||||
|
||||
private void ApplyCanvasSettings(Canvas canvas)
|
||||
{
|
||||
if (canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = _mainCamera;
|
||||
canvas.planeDistance = ResolveCanvasPlaneDistance(_mainCamera);
|
||||
|
||||
CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
|
||||
if (scaler != null)
|
||||
{
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = _referenceResolution;
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private static float ResolveCanvasPlaneDistance(Camera camera)
|
||||
{
|
||||
if (camera == null)
|
||||
{
|
||||
return DefaultCanvasPlaneDistance;
|
||||
}
|
||||
|
||||
float minDistance = camera.nearClipPlane + 0.05f;
|
||||
float maxDistance = camera.farClipPlane - 0.05f;
|
||||
if (maxDistance <= minDistance)
|
||||
{
|
||||
return minDistance;
|
||||
}
|
||||
|
||||
return Mathf.Clamp(DefaultCanvasPlaneDistance, minDistance, maxDistance);
|
||||
}
|
||||
|
||||
private void TryEnsureUiEventSubscribed()
|
||||
{
|
||||
if (_uiEventSubscribed || GameEntry.Event == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
|
||||
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
|
||||
_uiEventSubscribed = true;
|
||||
_canvasCacheDirty = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeUiEvents()
|
||||
{
|
||||
if (!_uiEventSubscribed || GameEntry.Event == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
|
||||
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
|
||||
_uiEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void OnUIFormChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
_canvasCacheDirty = true;
|
||||
}
|
||||
|
||||
private void UpdateMaskLayout()
|
||||
{
|
||||
if (!_enableBlackMask)
|
||||
{
|
||||
SetMaskVisible(false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureMaskObjects();
|
||||
if (_maskCanvas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float horizontalPadding = Mathf.Max(0f, (1f - _targetViewport.width) * 0.5f * _cachedScreenWidth);
|
||||
float verticalPadding = Mathf.Max(0f, (1f - _targetViewport.height) * 0.5f * _cachedScreenHeight);
|
||||
|
||||
bool showVerticalMask = horizontalPadding > 0.5f;
|
||||
bool showHorizontalMask = verticalPadding > 0.5f;
|
||||
|
||||
SetMaskVisible(showVerticalMask, showHorizontalMask);
|
||||
if (showVerticalMask)
|
||||
{
|
||||
SetVerticalMaskRect(_leftMask, true, horizontalPadding);
|
||||
SetVerticalMaskRect(_rightMask, false, horizontalPadding);
|
||||
}
|
||||
|
||||
if (showHorizontalMask)
|
||||
{
|
||||
SetHorizontalMaskRect(_topMask, true, verticalPadding);
|
||||
SetHorizontalMaskRect(_bottomMask, false, verticalPadding);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureMaskObjects()
|
||||
{
|
||||
if (_maskCanvas != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject maskRoot = new GameObject(
|
||||
"ResolutionMask",
|
||||
typeof(RectTransform),
|
||||
typeof(Canvas),
|
||||
typeof(CanvasScaler),
|
||||
typeof(GraphicRaycaster));
|
||||
maskRoot.transform.SetParent(transform, false);
|
||||
|
||||
_maskCanvas = maskRoot.GetComponent<Canvas>();
|
||||
_maskCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
_maskCanvas.overrideSorting = true;
|
||||
_maskCanvas.sortingOrder = short.MaxValue;
|
||||
|
||||
CanvasScaler scaler = maskRoot.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
|
||||
scaler.scaleFactor = 1f;
|
||||
|
||||
GraphicRaycaster raycaster = maskRoot.GetComponent<GraphicRaycaster>();
|
||||
raycaster.enabled = false;
|
||||
|
||||
RectTransform rootRect = maskRoot.GetComponent<RectTransform>();
|
||||
rootRect.anchorMin = Vector2.zero;
|
||||
rootRect.anchorMax = Vector2.one;
|
||||
rootRect.anchoredPosition = Vector2.zero;
|
||||
rootRect.sizeDelta = Vector2.zero;
|
||||
|
||||
_leftMask = CreateMaskRect("LeftMask", rootRect);
|
||||
_rightMask = CreateMaskRect("RightMask", rootRect);
|
||||
_topMask = CreateMaskRect("TopMask", rootRect);
|
||||
_bottomMask = CreateMaskRect("BottomMask", rootRect);
|
||||
}
|
||||
|
||||
private static RectTransform CreateMaskRect(string maskName, Transform parent)
|
||||
{
|
||||
GameObject maskObject = new GameObject(maskName, typeof(RectTransform), typeof(Image));
|
||||
maskObject.transform.SetParent(parent, false);
|
||||
|
||||
Image image = maskObject.GetComponent<Image>();
|
||||
image.color = Color.black;
|
||||
image.raycastTarget = false;
|
||||
|
||||
return maskObject.GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
private void SetMaskVisible(bool showVerticalMask, bool showHorizontalMask)
|
||||
{
|
||||
if (_maskCanvas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool visible = showVerticalMask || showHorizontalMask;
|
||||
_maskCanvas.enabled = visible;
|
||||
|
||||
if (_leftMask != null)
|
||||
{
|
||||
_leftMask.gameObject.SetActive(showVerticalMask);
|
||||
}
|
||||
|
||||
if (_rightMask != null)
|
||||
{
|
||||
_rightMask.gameObject.SetActive(showVerticalMask);
|
||||
}
|
||||
|
||||
if (_topMask != null)
|
||||
{
|
||||
_topMask.gameObject.SetActive(showHorizontalMask);
|
||||
}
|
||||
|
||||
if (_bottomMask != null)
|
||||
{
|
||||
_bottomMask.gameObject.SetActive(showHorizontalMask);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetVerticalMaskRect(RectTransform maskRect, bool isLeft, float width)
|
||||
{
|
||||
if (maskRect == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
maskRect.anchorMin = new Vector2(isLeft ? 0f : 1f, 0f);
|
||||
maskRect.anchorMax = new Vector2(isLeft ? 0f : 1f, 1f);
|
||||
maskRect.pivot = new Vector2(isLeft ? 0f : 1f, 0.5f);
|
||||
maskRect.anchoredPosition = Vector2.zero;
|
||||
maskRect.sizeDelta = new Vector2(width, 0f);
|
||||
}
|
||||
|
||||
private static void SetHorizontalMaskRect(RectTransform maskRect, bool isTop, float height)
|
||||
{
|
||||
if (maskRect == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
maskRect.anchorMin = new Vector2(0f, isTop ? 1f : 0f);
|
||||
maskRect.anchorMax = new Vector2(1f, isTop ? 1f : 0f);
|
||||
maskRect.pivot = new Vector2(0.5f, isTop ? 1f : 0f);
|
||||
maskRect.anchoredPosition = Vector2.zero;
|
||||
maskRect.sizeDelta = new Vector2(0f, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using GeometryTD.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class ShopNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
|
||||
private ShopFormUseCase _shopFormUseCase;
|
||||
private bool _initialized;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shopFormUseCase ??= new ShopFormUseCase();
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.ShopForm, _shopFormUseCase);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void StartShop(RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
OnInit();
|
||||
}
|
||||
|
||||
int runSeed = context?.RunSeed ?? 0;
|
||||
int sequenceIndex = context?.SequenceIndex ?? -1;
|
||||
if (_shopFormUseCase == null || !_shopFormUseCase.PrepareForOpen(runSeed, sequenceIndex))
|
||||
{
|
||||
Log.Warning("ShopNodeComponent.StartShop() failed. Shop use case is unavailable or goods generation failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.ShopForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeEnterEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
public void EndShop()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.ShopForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
_activeContext != null
|
||||
? _activeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
ClearActiveNodeContext();
|
||||
}
|
||||
|
||||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.Resource;
|
||||
using GeometryTD.CustomUtility;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class SpriteCacheComponent : GameFrameworkComponent
|
||||
{
|
||||
[SerializeField] private float _pixelsPerUnit = 100f;
|
||||
[SerializeField] private Vector2 _defaultPivot = new(0.5f, 0.5f);
|
||||
|
||||
private Dictionary<string, Sprite> _spriteCache;
|
||||
private Dictionary<string, List<Action<Sprite>>> _pendingCallbacks;
|
||||
private ResourceComponent _resource;
|
||||
|
||||
void Start()
|
||||
{
|
||||
_spriteCache = new Dictionary<string, Sprite>();
|
||||
_pendingCallbacks = new Dictionary<string, List<Action<Sprite>>>();
|
||||
_resource = GameEntry.Resource;
|
||||
}
|
||||
|
||||
public void GetSprite(string assetName, Action<Sprite> callback)
|
||||
{
|
||||
if (_spriteCache.TryGetValue(assetName, out var sprite))
|
||||
{
|
||||
callback?.Invoke(sprite);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pendingCallbacks.TryGetValue(assetName, out var pendingList))
|
||||
{
|
||||
pendingList.Add(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingCallbacks[assetName] = new List<Action<Sprite>> { callback };
|
||||
_resource.LoadAsset
|
||||
(
|
||||
AssetUtility.GetTextureAsset(assetName),
|
||||
Constant.AssetPriority.UIFormAsset,
|
||||
new LoadAssetCallbacks(
|
||||
(resourcePath, asset, duration, userData) =>
|
||||
{
|
||||
Log.Debug(resourcePath);
|
||||
Texture2D texture = asset as Texture2D;
|
||||
Sprite loadedSprite = null;
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
loadedSprite = Sprite.Create(
|
||||
texture,
|
||||
new Rect(0, 0, texture.width, texture.height),
|
||||
_defaultPivot,
|
||||
_pixelsPerUnit);
|
||||
|
||||
_spriteCache[assetName] = loadedSprite;
|
||||
}
|
||||
|
||||
if (_pendingCallbacks.TryGetValue(assetName, out var callbacks))
|
||||
{
|
||||
_pendingCallbacks.Remove(assetName);
|
||||
for (int i = 0; i < callbacks.Count; i++)
|
||||
{
|
||||
callbacks[i]?.Invoke(loadedSprite);
|
||||
}
|
||||
}
|
||||
},
|
||||
(resourcePath, status, errorMessage, userData) =>
|
||||
{
|
||||
Log.Error("Can not load icon '{0}' from '{1}' with error message '{2}'.",
|
||||
assetName,
|
||||
resourcePath,
|
||||
errorMessage);
|
||||
|
||||
if (_pendingCallbacks.TryGetValue(assetName, out var callbacks))
|
||||
{
|
||||
_pendingCallbacks.Remove(assetName);
|
||||
for (int i = 0; i < callbacks.Count; i++)
|
||||
{
|
||||
callbacks[i]?.Invoke(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_spriteCache.Clear();
|
||||
_pendingCallbacks.Clear();
|
||||
_resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class TagRegistryComponent : GameFrameworkComponent
|
||||
{
|
||||
private IDataTable<DRTagConfig> _tagConfigTable;
|
||||
private IDataTable<DRTag> _tagTable;
|
||||
private IDataTable<DRRarityTagBudget> _rarityTagBudgetTable;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
ReloadAllFromLoadedTables();
|
||||
}
|
||||
|
||||
public void ReloadAllFromLoadedTables()
|
||||
{
|
||||
ReloadTagDefinitionsAndGenerationFromLoadedTables();
|
||||
ReloadRarityTagBudgetFromLoadedTable();
|
||||
}
|
||||
|
||||
public void ReloadTagDefinitionsAndGenerationFromLoadedTables()
|
||||
{
|
||||
EnsureTagDefinitionTables();
|
||||
DRTagConfig[] tagConfigRows = _tagConfigTable.GetAllDataRows();
|
||||
DRTag[] tagRows = _tagTable.GetAllDataRows();
|
||||
|
||||
TagGenerationRuleRegistry.LoadFromRows(tagRows);
|
||||
TagDefinitionRegistry.ReloadFromRows(tagConfigRows, tagRows);
|
||||
}
|
||||
|
||||
public void ReloadRarityTagBudgetFromLoadedTable()
|
||||
{
|
||||
EnsureRarityTagBudgetTable();
|
||||
RarityTagBudgetRuleRegistry.LoadFromRows(_rarityTagBudgetTable.GetAllDataRows());
|
||||
}
|
||||
|
||||
private void EnsureTagDefinitionTables()
|
||||
{
|
||||
_tagConfigTable ??= GameEntry.DataTable.GetDataTable<DRTagConfig>();
|
||||
_tagTable ??= GameEntry.DataTable.GetDataTable<DRTag>();
|
||||
|
||||
if (_tagConfigTable == null || _tagTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TagRegistryComponent requires TagConfig and Tag data tables to be loaded before initialization.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRarityTagBudgetTable()
|
||||
{
|
||||
_rarityTagBudgetTable ??= GameEntry.DataTable.GetDataTable<DRRarityTagBudget>();
|
||||
if (_rarityTagBudgetTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TagRegistryComponent requires RarityTagBudget data table to be loaded before initialization.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
using GeometryTD.UI;
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class UIRouterComponent : GameFrameworkComponent
|
||||
{
|
||||
private readonly Dictionary<UIFormType, IUIFormController> _routeControllers = new();
|
||||
|
||||
private const string UINameSpace = "GeometryTD.UI";
|
||||
|
||||
public void BindUIUseCase(UIFormType uiFormType, IUIUseCase useCase)
|
||||
{
|
||||
IUIFormController controller = GetOrCreateController(uiFormType);
|
||||
controller.BindUseCase(useCase);
|
||||
}
|
||||
|
||||
public int? OpenUI(UIFormType uiFormType, object userData = null)
|
||||
{
|
||||
IUIFormController controller = GetOrCreateController(uiFormType);
|
||||
return controller.OpenUI(userData);
|
||||
}
|
||||
|
||||
public void CloseUI(UIFormType uiFormType)
|
||||
{
|
||||
IUIFormController controller = GetOrCreateController(uiFormType);
|
||||
controller.CloseUI();
|
||||
}
|
||||
|
||||
private IUIFormController GetOrCreateController(UIFormType uiFormType)
|
||||
{
|
||||
if (_routeControllers.TryGetValue(uiFormType, out IUIFormController controller))
|
||||
{
|
||||
return controller;
|
||||
}
|
||||
|
||||
string typename = $"{UINameSpace}.{uiFormType}Controller";
|
||||
Type controllerType = Type.GetType(typename);
|
||||
if (controllerType == null)
|
||||
{
|
||||
controller = new DefaultUIFormController(uiFormType);
|
||||
Log.Warning("Can not find UI Controller for type '{0}'.", typename);
|
||||
}
|
||||
else
|
||||
{
|
||||
controller = (IUIFormController)Activator.CreateInstance(controllerType);
|
||||
}
|
||||
|
||||
_routeControllers.Add(uiFormType, controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (KeyValuePair<UIFormType, IUIFormController> pair in _routeControllers)
|
||||
{
|
||||
pair.Value.CloseUI();
|
||||
}
|
||||
|
||||
_routeControllers.Clear();
|
||||
}
|
||||
|
||||
private class DefaultUIFormController : IUIFormController
|
||||
{
|
||||
private readonly UIFormType _uiFormType;
|
||||
private int? _lastSerialId;
|
||||
|
||||
public DefaultUIFormController(UIFormType uiFormType)
|
||||
{
|
||||
_uiFormType = uiFormType;
|
||||
}
|
||||
|
||||
public int? OpenUI(object userData = null)
|
||||
{
|
||||
_lastSerialId = GameEntry.UI.OpenUIForm(_uiFormType, userData);
|
||||
return _lastSerialId;
|
||||
}
|
||||
|
||||
public void CloseUI()
|
||||
{
|
||||
if (_lastSerialId.HasValue)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(_lastSerialId.Value);
|
||||
_lastSerialId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
UGuiForm uiForm = GameEntry.UI.GetUIForm(_uiFormType);
|
||||
if (uiForm != null)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(uiForm);
|
||||
}
|
||||
}
|
||||
|
||||
public void BindUseCase(IUIUseCase useCase)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user