refactor 5:

- MapData.cs 现在会携带战斗初始上下文:
    - InitialCoin
    - 建塔 TowerStatsData 快照
    - TryConsumeCoin / AddCoin 运行时回调
- CombatLoadingState.cs 负责从 CombatInRunResourceManager 读取 coin 和 build stats 快照,组装 MapData 后再交给加载链。
- CombatLoadSession.cs 改成接收外部构造好的 MapData,不再自己只塞一个 LevelId。
- CombatScheduler.cs 不再在 Start() 里直接发起地图加载,加载细节回到 CombatLoadingState。
- MapEntity.cs 已经不再直接读 GameEntry.CombatNode 的 coin / build count / build stats,也不再通过它做 coin 读写:
    - 初始 coin 和 build stats 从 MapData 读取
    - 后续 coin 通过 CombatCoinChangedEventArgs 同步
    - 建塔/升级/拆塔时的 coin 变更通过 MapData 注入回调执行
This commit is contained in:
SepComet 2026-03-07 15:12:18 +08:00
parent ccb4738b96
commit ca7b2f2dca
5 changed files with 213 additions and 22 deletions

View File

@ -45,7 +45,7 @@ namespace GeometryTD.CustomComponent
_currentMap = null; _currentMap = null;
} }
public bool StartLoading(DRLevel level, out string errorMessage) public bool StartLoading(DRLevel level, MapData mapData, out string errorMessage)
{ {
errorMessage = null; errorMessage = null;
if (_entity == null) if (_entity == null)
@ -54,7 +54,7 @@ namespace GeometryTD.CustomComponent
return false; return false;
} }
if (!TryShowMap(level, out errorMessage)) if (!TryShowMap(level, mapData, out errorMessage))
{ {
return false; return false;
} }
@ -224,7 +224,7 @@ namespace GeometryTD.CustomComponent
} }
} }
private bool TryShowMap(DRLevel level, out string errorMessage) private bool TryShowMap(DRLevel level, MapData mapData, out string errorMessage)
{ {
errorMessage = null; errorMessage = null;
if (level == null) if (level == null)
@ -242,7 +242,10 @@ namespace GeometryTD.CustomComponent
} }
_loadingMapEntityId = _entity.GenerateSerialId(); _loadingMapEntityId = _entity.GenerateSerialId();
_entity.ShowMap(new MapData(_loadingMapEntityId, level.Id, Vector3.zero), mapAssetName); MapData resolvedMapData = mapData != null
? mapData.CloneForEntity(_loadingMapEntityId, Vector3.zero)
: new MapData(_loadingMapEntityId, level.Id, Vector3.zero);
_entity.ShowMap(resolvedMapData, mapAssetName);
return true; return true;
} }

View File

@ -120,11 +120,6 @@ namespace GeometryTD.CustomComponent
return HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data."); return HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data.");
} }
if (!_loadSession.StartLoading(level, out string loadError))
{
return HandleStartFailure($"CombatScheduler start failed. {loadError}");
}
ChangeState(new CombatLoadingState(this)); ChangeState(new CombatLoadingState(this));
Log.Info( Log.Info(
"CombatScheduler started. Level={0}, PhaseCount={1}.", "CombatScheduler started. Level={0}, PhaseCount={1}.",

View File

@ -1,3 +1,8 @@
using System.Collections.Generic;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
using UnityEngine;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
public partial class CombatScheduler public partial class CombatScheduler
@ -8,6 +13,21 @@ namespace GeometryTD.CustomComponent
{ {
} }
public override void OnEnter()
{
if (Scheduler._currentLevel == null)
{
Scheduler.EnterFailureFallback("Combat loading failed. Current level is null.");
return;
}
MapData mapData = BuildMapData();
if (!Scheduler._loadSession.StartLoading(Scheduler._currentLevel, mapData, out string errorMessage))
{
Scheduler.EnterFailureFallback($"Combat loading failed. {errorMessage}");
}
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds) public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{ {
_ = elapseSeconds; _ = elapseSeconds;
@ -20,6 +40,29 @@ namespace GeometryTD.CustomComponent
Scheduler.TryBeginNextPhase(); Scheduler.TryBeginNextPhase();
} }
private MapData BuildMapData()
{
List<TowerStatsData> buildTowerStatsSnapshot = new();
for (int i = 0; i < Scheduler._combatInRunResourceManager.CurrentBuildTowerCount; i++)
{
if (Scheduler._combatInRunResourceManager.TryGetBuildTowerStats(i, out TowerStatsData stats) &&
stats != null)
{
buildTowerStatsSnapshot.Add(stats);
}
}
return new MapData(
entityId: 0,
typeId: 0,
levelId: Scheduler._currentLevel.Id,
position: Vector3.zero,
initialCoin: Scheduler._combatInRunResourceManager.CurrentCoin,
buildTowerStatsSnapshot: buildTowerStatsSnapshot,
tryConsumeCoin: Scheduler.TryConsumeCoin,
addCoin: Scheduler.AddCoin);
}
} }
} }
} }

View File

@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using GeometryTD.Definition;
using GeometryTD.CustomUtility;
using UnityEngine; using UnityEngine;
namespace GeometryTD.Entity.EntityData namespace GeometryTD.Entity.EntityData
@ -7,6 +10,11 @@ namespace GeometryTD.Entity.EntityData
public class MapData : EntityDataBase public class MapData : EntityDataBase
{ {
[SerializeField] private int _levelId = 0; [SerializeField] private int _levelId = 0;
[SerializeField] private int _initialCoin = 0;
[SerializeField] private TowerStatsData[] _buildTowerStatsSnapshot = Array.Empty<TowerStatsData>();
[NonSerialized] private Func<int, bool> _tryConsumeCoin;
[NonSerialized] private Action<int> _addCoin;
public MapData(int entityId, int levelId, Vector3 position) : this(entityId, 0, levelId, position) public MapData(int entityId, int levelId, Vector3 position) : this(entityId, 0, levelId, position)
{ {
@ -18,10 +26,110 @@ namespace GeometryTD.Entity.EntityData
Position = position; Position = position;
} }
public MapData(
int entityId,
int typeId,
int levelId,
Vector3 position,
int initialCoin,
IReadOnlyList<TowerStatsData> buildTowerStatsSnapshot,
Func<int, bool> tryConsumeCoin,
Action<int> addCoin) : base(entityId, typeId)
{
_levelId = levelId;
Position = position;
_initialCoin = Mathf.Max(0, initialCoin);
SetBuildTowerStatsSnapshot(buildTowerStatsSnapshot);
_tryConsumeCoin = tryConsumeCoin;
_addCoin = addCoin;
}
public int LevelId public int LevelId
{ {
get => _levelId; get => _levelId;
set => _levelId = value; set => _levelId = value;
} }
public int InitialCoin
{
get => _initialCoin;
set => _initialCoin = Mathf.Max(0, value);
}
public int CurrentBuildTowerCount => _buildTowerStatsSnapshot != null ? _buildTowerStatsSnapshot.Length : 0;
public void BindCombatCallbacks(Func<int, bool> tryConsumeCoin, Action<int> addCoin)
{
_tryConsumeCoin = tryConsumeCoin;
_addCoin = addCoin;
}
public bool TryConsumeCoin(int coin)
{
int requiredCoin = Mathf.Max(0, coin);
if (requiredCoin <= 0)
{
return true;
}
return _tryConsumeCoin != null && _tryConsumeCoin.Invoke(requiredCoin);
}
public void AddCoin(int coin)
{
int amount = Mathf.Max(0, coin);
if (amount <= 0)
{
return;
}
_addCoin?.Invoke(amount);
}
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
{
stats = null;
if (_buildTowerStatsSnapshot == null || buildIndex < 0 || buildIndex >= _buildTowerStatsSnapshot.Length)
{
return false;
}
TowerStatsData sourceStats = _buildTowerStatsSnapshot[buildIndex];
if (sourceStats == null)
{
return false;
}
stats = InventoryCloneUtility.CloneTowerStats(sourceStats);
return stats != null;
}
public void SetBuildTowerStatsSnapshot(IReadOnlyList<TowerStatsData> buildTowerStatsSnapshot)
{
if (buildTowerStatsSnapshot == null || buildTowerStatsSnapshot.Count <= 0)
{
_buildTowerStatsSnapshot = Array.Empty<TowerStatsData>();
return;
}
_buildTowerStatsSnapshot = new TowerStatsData[buildTowerStatsSnapshot.Count];
for (int i = 0; i < buildTowerStatsSnapshot.Count; i++)
{
_buildTowerStatsSnapshot[i] = InventoryCloneUtility.CloneTowerStats(buildTowerStatsSnapshot[i]);
}
}
public MapData CloneForEntity(int entityId, Vector3 position)
{
return new MapData(
entityId,
TypeId,
_levelId,
position,
_initialCoin,
_buildTowerStatsSnapshot,
_tryConsumeCoin,
_addCoin);
}
} }
} }

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.CustomComponent; using GeometryTD.CustomComponent;
using GeometryTD.CustomEvent;
using GameFramework.Event;
using GeometryTD.Map; using GeometryTD.Map;
using GeometryTD.Definition; using GeometryTD.Definition;
using GeometryTD.Entity.EntityData; using GeometryTD.Entity.EntityData;
@ -37,6 +39,8 @@ namespace GeometryTD.Entity
private TowerSelectionPresenter _towerSelectionPresenter; private TowerSelectionPresenter _towerSelectionPresenter;
private CombatSelectUseCaseConfigurator _combatSelectUseCaseConfigurator; private CombatSelectUseCaseConfigurator _combatSelectUseCaseConfigurator;
private int _currentCoin;
private bool _isCoinEventSubscribed;
public IReadOnlyList<Vector3Int> PathCells => _mapTopologyService != null public IReadOnlyList<Vector3Int> PathCells => _mapTopologyService != null
? _mapTopologyService.PathCells ? _mapTopologyService.PathCells
@ -117,6 +121,11 @@ namespace GeometryTD.Entity
{ {
Log.Warning("MapData is invalid for map entity '{0}'.", Id); Log.Warning("MapData is invalid for map entity '{0}'.", Id);
} }
else
{
_currentCoin = Mathf.Max(0, _mapData.InitialCoin);
SubscribeCombatEvents();
}
RefreshTiles(); RefreshTiles();
ConfigureCombatSelectUseCase(); ConfigureCombatSelectUseCase();
@ -125,6 +134,7 @@ namespace GeometryTD.Entity
protected override void OnHide(bool isShutdown, object userData) protected override void OnHide(bool isShutdown, object userData)
{ {
UnsubscribeCombatEvents();
HideCombatSelectForm(); HideCombatSelectForm();
_towerPlacementService?.HideAndClearAllPlacedTowers(); _towerPlacementService?.HideAndClearAllPlacedTowers();
ClearSelectionState(); ClearSelectionState();
@ -301,8 +311,7 @@ namespace GeometryTD.Entity
return false; return false;
} }
CombatNodeComponent combatNode = GameEntry.CombatNode; if (_mapData == null || !_mapData.TryGetBuildTowerStats(buildIndex, out TowerStatsData buildTowerStats))
if (combatNode == null || !combatNode.TryGetBuildTowerStats(buildIndex, out TowerStatsData buildTowerStats))
{ {
return false; return false;
} }
@ -376,17 +385,49 @@ namespace GeometryTD.Entity
GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm); GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm);
} }
private static int GetCurrentBuildTowerCount() private void SubscribeCombatEvents()
{ {
if (GameEntry.CombatNode == null) if (_isCoinEventSubscribed)
{
return;
}
GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged);
_isCoinEventSubscribed = true;
}
private void UnsubscribeCombatEvents()
{
if (!_isCoinEventSubscribed)
{
return;
}
GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged);
_isCoinEventSubscribed = false;
}
private void OnCombatCoinChanged(object sender, GameEventArgs e)
{
if (e is not CombatCoinChangedEventArgs args)
{
return;
}
_currentCoin = Mathf.Max(0, args.CurrentCoin);
}
private int GetCurrentBuildTowerCount()
{
if (_mapData == null)
{ {
return 0; return 0;
} }
return Mathf.Clamp(GameEntry.CombatNode.CurrentBuildTowerCount, 0, 4); return Mathf.Clamp(_mapData.CurrentBuildTowerCount, 0, 4);
} }
private static bool TryConsumeCoin(int cost) private bool TryConsumeCoin(int cost)
{ {
int requiredCoin = Mathf.Max(0, cost); int requiredCoin = Mathf.Max(0, cost);
if (requiredCoin <= 0) if (requiredCoin <= 0)
@ -394,27 +435,28 @@ namespace GeometryTD.Entity
return true; return true;
} }
if (GameEntry.CombatNode == null) if (_mapData == null)
{ {
return false; return false;
} }
return GameEntry.CombatNode.TryConsumeCoin(requiredCoin); return _mapData.TryConsumeCoin(requiredCoin);
} }
private static int GetCurrentCoin() private int GetCurrentCoin()
{ {
return GameEntry.CombatNode != null ? Mathf.Max(0, GameEntry.CombatNode.CurrentCoin) : 0; return Mathf.Max(0, _currentCoin);
} }
private static void AddCoin(int coin) private void AddCoin(int coin)
{ {
int amount = Mathf.Max(0, coin); int amount = Mathf.Max(0, coin);
if (amount <= 0) if (amount <= 0)
{ {
return; return;
} }
GameEntry.CombatNode?.AddCoin(amount);
_mapData?.AddCoin(amount);
} }
} }
} }