MapData + Event 解耦已完成一轮收口

- `MapData` 已收口为纯初始化快照,不再承载 coin 写接口委托
- 已新增 `MapEntityLoadContext`
  - 用于把 `MapData` 快照与 coin 命令通道拆开传给地图加载
- `CombatLoadingState` 现在会组装:
  - `MapData`
  - `MapEntityLoadContext`
- `CombatLoadSession` / `EntityExtension.ShowMap(...)` 已切到 `MapEntityLoadContext`
- `MapEntity` 当前通过:
  - `MapEntityLoadContext` 获取初始快照与 coin 命令通道
  - `CombatCoinChangedEventArgs` 同步后续 coin 变化
- 已新增 `MapCombatRuntimeBridge`
  - 收口地图侧 coin 当前值、命令调用与事件订阅
- `MapEntity` 不再自己维护 `_currentCoin` 和 coin 事件订阅样板
This commit is contained in:
2026-03-07 20:00:39 +08:00
parent b38088c3ea
commit 3ad7d04b47
15 changed files with 224 additions and 121 deletions
@@ -45,7 +45,7 @@ namespace GeometryTD.CustomComponent
_currentMap = null;
}
public bool StartLoading(DRLevel level, MapEntityLoadContext mapLoadContext, CombatScheduler scheduler, out string errorMessage)
public bool StartLoading(DRLevel level, MapEntityLoadContext mapLoadContext, ICombatSchedulerHost schedulerHost, out string errorMessage)
{
errorMessage = null;
if (_entity == null)
@@ -59,7 +59,7 @@ namespace GeometryTD.CustomComponent
return false;
}
if (!TryOpenCombatInfoForm(scheduler, out errorMessage))
if (!TryOpenCombatInfoForm(schedulerHost, out errorMessage))
{
return false;
}
@@ -253,7 +253,7 @@ namespace GeometryTD.CustomComponent
return true;
}
private bool TryOpenCombatInfoForm(CombatScheduler scheduler, out string errorMessage)
private bool TryOpenCombatInfoForm(ICombatSchedulerHost schedulerHost, out string errorMessage)
{
errorMessage = null;
if (_combatInfoFormUseCase == null)
@@ -263,9 +263,9 @@ namespace GeometryTD.CustomComponent
}
_combatInfoFormUseCase.Configure(
() => BuildCombatInfoFormRawData(scheduler),
() => scheduler != null && scheduler.CanEndCombat && scheduler.TryEndCombatByPlayer(),
() => scheduler != null && scheduler.TryDebugFail("Manual debug fail from CombatInfoForm."));
() => BuildCombatInfoFormRawData(schedulerHost),
() => schedulerHost != null && schedulerHost.CanEndCombat && schedulerHost.TryEndCombatByPlayer(),
() => schedulerHost != null && schedulerHost.TryDebugFail("Manual debug fail from CombatInfoForm."));
int? serialId = GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
if (!serialId.HasValue)
@@ -279,26 +279,26 @@ namespace GeometryTD.CustomComponent
return true;
}
private static CombatInfoFormRawData BuildCombatInfoFormRawData(CombatScheduler scheduler)
private static CombatInfoFormRawData BuildCombatInfoFormRawData(ICombatSchedulerHost schedulerHost)
{
if (scheduler == null)
if (schedulerHost == null)
{
return null;
}
DRLevel level = scheduler.CurrentLevel;
DRLevel level = schedulerHost.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,
scheduler.DisplayPhaseIndex,
scheduler.PhaseCount,
scheduler.CurrentCoin,
scheduler.CurrentBaseHp,
schedulerHost.DisplayPhaseIndex,
schedulerHost.PhaseCount,
schedulerHost.CurrentCoin,
schedulerHost.CurrentBaseHp,
baseHpMax,
scheduler.CanEndCombat);
schedulerHost.CanEndCombat);
}
private void CloseCombatInfoForm()
@@ -9,7 +9,7 @@ using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public partial class CombatScheduler
public partial class CombatScheduler : ICombatSchedulerHost
{
private readonly CombatSchedulerRuntimeContext _context = new();
private readonly CombatSchedulerFlowCoordinator _flowCoordinator;
@@ -237,6 +237,11 @@ namespace GeometryTD.CustomComponent
return _context.CurrentState is CombatFailedState;
}
void ICombatSchedulerHost.ChangeState(CombatStateBase nextState)
{
ChangeState(nextState);
}
internal void ChangeState(CombatStateBase nextState)
{
if (ReferenceEquals(_context.CurrentState, nextState))
@@ -10,17 +10,21 @@ namespace GeometryTD.CustomComponent
{
internal sealed class CombatSchedulerFlowCoordinator
{
private readonly CombatScheduler _scheduler;
private readonly ICombatSchedulerHost _schedulerHost;
private readonly CombatSchedulerRuntimeContext _context;
public ICombatSchedulerHost Host => _schedulerHost;
public CombatScheduler Scheduler => _scheduler;
public CombatSchedulerFlowCoordinator(CombatScheduler scheduler, CombatSchedulerRuntimeContext context)
public CombatSchedulerFlowCoordinator(ICombatSchedulerHost schedulerHost, CombatSchedulerRuntimeContext context)
{
_scheduler = scheduler;
_schedulerHost = schedulerHost;
_context = context;
}
public void ChangeState(CombatStateBase nextState)
{
_schedulerHost.ChangeState(nextState);
}
public int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount)
{
if (displayPhaseIndex <= 0 || phaseCount <= 0)
@@ -60,7 +64,7 @@ namespace GeometryTD.CustomComponent
public void EnsureCombatFinishFormUseCaseBound()
{
_context.CombatFinishFormUseCase ??= new CombatFinishFormUseCase(_scheduler);
_context.CombatFinishFormUseCase ??= new CombatFinishFormUseCase(_schedulerHost);
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _context.CombatFinishFormUseCase);
}
@@ -88,18 +92,18 @@ namespace GeometryTD.CustomComponent
{
if (!_context.PhaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
{
_scheduler.ChangeState(new CombatSettlementState(_context, this, "Combat ended after loop completion.", true));
_schedulerHost.ChangeState(new CombatSettlementState(_context, this, "Combat ended after loop completion.", true));
return false;
}
_context.SpawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries);
_scheduler.ChangeState(new CombatRunningPhaseState(_context, this, nextPhase, spawnEntries));
_schedulerHost.ChangeState(new CombatRunningPhaseState(_context, this, nextPhase, spawnEntries));
return true;
}
public void EnterWaitingForPhaseEnd()
{
_scheduler.ChangeState(new CombatWaitingForPhaseEndState(_context, this));
_schedulerHost.ChangeState(new CombatWaitingForPhaseEndState(_context, this));
}
public void CompleteCurrentPhase()
@@ -143,7 +147,7 @@ namespace GeometryTD.CustomComponent
}
_context.SettlementFlowService.ApplySelectedReward(_context.SettlementContext, selectedReward);
_scheduler.ChangeState(new CombatFinishFormState(_context, this));
_schedulerHost.ChangeState(new CombatFinishFormState(_context, this));
}
public void OnFullBaseHpRewardGiveUp()
@@ -153,7 +157,7 @@ namespace GeometryTD.CustomComponent
return;
}
_scheduler.ChangeState(new CombatFinishFormState(_context, this));
_schedulerHost.ChangeState(new CombatFinishFormState(_context, this));
}
public LevelThemeType ResolveCurrentThemeType()
@@ -176,6 +180,16 @@ namespace GeometryTD.CustomComponent
return _context.CombatInRunResourceManager.ApplyBaseDamage(damage);
}
public bool TryConsumeCoin(int coin)
{
return _schedulerHost.TryConsumeCoin(coin);
}
public void AddCoin(int coin)
{
_schedulerHost.AddCoin(coin);
}
public int GetCurrentBaseHp()
{
return Mathf.Max(0, _context.CombatInRunResourceManager.CurrentBaseHp);
@@ -231,7 +245,7 @@ namespace GeometryTD.CustomComponent
return;
}
_scheduler.ChangeState(new CombatFailedState(_context, this, errorMessage));
_schedulerHost.ChangeState(new CombatFailedState(_context, this, errorMessage));
}
public void OnCombatFailureDialogConfirmed(object userData)
@@ -3,6 +3,20 @@ using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatSettlementContext
{
public CombatSettlementFlowState Flow { get; } = new();
public CombatSettlementResult Result { get; } = new();
public CombatSettlementSummary Summary { get; } = new();
}
internal sealed class CombatSettlementFlowState
{
public bool ShouldOpenRewardSelection;
public bool DidEnterRewardSelection;
public bool IsCommitted;
}
internal sealed class CombatSettlementResult
{
public bool IsVictory;
public int FinalCoin;
@@ -11,12 +25,21 @@ namespace GeometryTD.CustomComponent
public int DefeatedEnemyCount;
public int GainedGold;
public BackpackInventoryData RewardInventory;
public bool ShouldOpenRewardSelection;
public bool DidEnterRewardSelection;
public string Reason;
public CombatSettlementPenaltyResult Penalty { get; } = new();
}
internal sealed class CombatSettlementPenaltyResult
{
public bool ShouldApplyLowBaseHpPenalty;
public float LowBaseHpEndurancePenaltyValue;
public int AffectedTowerCount;
public bool IsCommitted;
public string Reason;
}
internal sealed class CombatSettlementSummary
{
public int DefeatedEnemyCount;
public int GainedGold;
public BackpackInventoryData RewardInventory;
}
}
@@ -39,22 +39,25 @@ namespace GeometryTD.CustomComponent
CombatSettlementContext settlementContext = new CombatSettlementContext
{
IsVictory = isVictory,
FinalCoin = Mathf.Max(0, resourceManager != null ? resourceManager.CurrentCoin : 0),
FinalBaseHp = currentBaseHp,
MaxBaseHp = maxBaseHp,
DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount),
GainedGold = Mathf.Max(0, resourceManager != null ? resourceManager.GainedGold : 0),
RewardInventory = resourceManager != null
? resourceManager.GetRewardInventorySnapshot()
: new BackpackInventoryData(),
ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect,
DidEnterRewardSelection = false,
ShouldApplyLowBaseHpPenalty = appliedLowBaseHpPenalty,
LowBaseHpEndurancePenaltyValue = appliedLowBaseHpPenalty ? LowBaseHpTowerEndurancePenalty : 0f,
AffectedTowerCount = 0,
Reason = reason
};
settlementContext.Result.IsVictory = isVictory;
settlementContext.Result.FinalCoin = Mathf.Max(0, resourceManager != null ? resourceManager.CurrentCoin : 0);
settlementContext.Result.FinalBaseHp = currentBaseHp;
settlementContext.Result.MaxBaseHp = maxBaseHp;
settlementContext.Result.DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount);
settlementContext.Result.GainedGold = Mathf.Max(0, resourceManager != null ? resourceManager.GainedGold : 0);
settlementContext.Result.RewardInventory = resourceManager != null
? resourceManager.GetRewardInventorySnapshot()
: new BackpackInventoryData();
settlementContext.Result.Reason = reason;
settlementContext.Result.Penalty.ShouldApplyLowBaseHpPenalty = appliedLowBaseHpPenalty;
settlementContext.Result.Penalty.LowBaseHpEndurancePenaltyValue =
appliedLowBaseHpPenalty ? LowBaseHpTowerEndurancePenalty : 0f;
settlementContext.Flow.ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect;
settlementContext.Flow.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}, Reason={1}, BaseHp={2}/{3}, LevelReward={4}, BonusRate={5:P0}, BonusGold={6}, FullHpRewardSelect={7}, LowHpPenalty={8}.",
@@ -72,16 +75,17 @@ namespace GeometryTD.CustomComponent
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
{
if (settlementContext == null || settlementContext.IsCommitted)
if (settlementContext == null || settlementContext.Flow.IsCommitted)
{
return;
}
BackpackInventoryData rewardInventory = settlementContext.RewardInventory ?? new BackpackInventoryData();
BackpackInventoryData rewardInventory = settlementContext.Result.RewardInventory ?? new BackpackInventoryData();
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
settlementContext.RewardInventory = rewardInventory;
settlementContext.AffectedTowerCount = ApplyDeferredSettlementPenalty(settlementContext);
settlementContext.IsCommitted = true;
settlementContext.Result.RewardInventory = rewardInventory;
settlementContext.Summary.RewardInventory = rewardInventory;
settlementContext.Result.Penalty.AffectedTowerCount = ApplyDeferredSettlementPenalty(settlementContext);
settlementContext.Flow.IsCommitted = true;
}
public bool TryPrepareRewardSelection(
@@ -104,7 +108,7 @@ namespace GeometryTD.CustomComponent
RewardSelectDisplayCount);
if (candidateItems == null || candidateItems.Count <= 0)
{
settlementContext.ShouldOpenRewardSelection = false;
settlementContext.Flow.ShouldOpenRewardSelection = false;
return false;
}
@@ -122,7 +126,7 @@ namespace GeometryTD.CustomComponent
if (rewardPool.Count <= 0)
{
settlementContext.ShouldOpenRewardSelection = false;
settlementContext.Flow.ShouldOpenRewardSelection = false;
return false;
}
@@ -138,23 +142,24 @@ namespace GeometryTD.CustomComponent
RewardSelectFormRawData rawData = rewardSelectFormUseCase.CreateInitialModel();
if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0)
{
settlementContext.ShouldOpenRewardSelection = false;
settlementContext.Flow.ShouldOpenRewardSelection = false;
return false;
}
settlementContext.DidEnterRewardSelection = true;
settlementContext.Flow.DidEnterRewardSelection = true;
GameEntry.UIRouter.OpenUI(UIFormType.RewardSelectForm, rawData);
return true;
}
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
{
if (settlementContext?.RewardInventory == null || selectedReward?.SourceItem == null)
if (settlementContext?.Result.RewardInventory == null || selectedReward?.SourceItem == null)
{
return;
}
TryAppendRewardComponent(settlementContext.RewardInventory, selectedReward.SourceItem);
TryAppendRewardComponent(settlementContext.Result.RewardInventory, selectedReward.SourceItem);
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
}
public void OpenCombatFinishForm(
@@ -227,8 +232,8 @@ namespace GeometryTD.CustomComponent
private static int ApplyDeferredSettlementPenalty(CombatSettlementContext settlementContext)
{
if (settlementContext == null ||
!settlementContext.ShouldApplyLowBaseHpPenalty ||
settlementContext.LowBaseHpEndurancePenaltyValue <= 0f)
!settlementContext.Result.Penalty.ShouldApplyLowBaseHpPenalty ||
settlementContext.Result.Penalty.LowBaseHpEndurancePenaltyValue <= 0f)
{
return 0;
}
@@ -239,7 +244,7 @@ namespace GeometryTD.CustomComponent
return 0;
}
return inventory.ReduceAllTowerEndurance(settlementContext.LowBaseHpEndurancePenaltyValue);
return inventory.ReduceAllTowerEndurance(settlementContext.Result.Penalty.LowBaseHpEndurancePenaltyValue);
}
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)
@@ -20,7 +20,7 @@ namespace GeometryTD.CustomComponent
Context.SettlementFlowService.OpenCombatFinishForm(
Context.SettlementContext,
Context.CombatFinishFormUseCase);
Flow.Scheduler.ChangeState(new CombatWaitingForReturnState(Context, Flow));
Flow.ChangeState(new CombatWaitingForReturnState(Context, Flow));
}
}
}
@@ -22,7 +22,7 @@ namespace GeometryTD.CustomComponent
}
MapEntityLoadContext mapLoadContext = BuildMapLoadContext();
if (!Context.LoadSession.StartLoading(Context.CurrentLevel, mapLoadContext, Flow.Scheduler, out string errorMessage))
if (!Context.LoadSession.StartLoading(Context.CurrentLevel, mapLoadContext, Flow.Host, out string errorMessage))
{
Flow.EnterFailureFallback($"Combat loading failed. {errorMessage}");
}
@@ -66,7 +66,7 @@ namespace GeometryTD.CustomComponent
participantTowerSnapshot: GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetParticipantTowerSnapshot()
: null);
return new MapEntityLoadContext(mapData, Flow.Scheduler.TryConsumeCoin, Flow.Scheduler.AddCoin);
return new MapEntityLoadContext(mapData, Flow.TryConsumeCoin, Flow.AddCoin);
}
}
}
@@ -25,7 +25,7 @@ namespace GeometryTD.CustomComponent
Flow.OnFullBaseHpRewardSelected,
Flow.OnFullBaseHpRewardGiveUp))
{
Flow.Scheduler.ChangeState(new CombatFinishFormState(Context, Flow));
Flow.ChangeState(new CombatFinishFormState(Context, Flow));
}
}
@@ -62,7 +62,7 @@ namespace GeometryTD.CustomComponent
if (Flow.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory))
{
Flow.Scheduler.ChangeState(new CombatSettlementState(Context, Flow, reason, isVictory));
Flow.ChangeState(new CombatSettlementState(Context, Flow, reason, isVictory));
return;
}
@@ -26,13 +26,13 @@ namespace GeometryTD.CustomComponent
Context.CurrentLevel,
Context.EnemyManager.DefeatedEnemyCount,
Context.CombatInRunResourceManager);
if (Context.SettlementContext.ShouldOpenRewardSelection)
if (Context.SettlementContext.Flow.ShouldOpenRewardSelection)
{
Flow.Scheduler.ChangeState(new CombatRewardSelectionState(Context, Flow));
Flow.ChangeState(new CombatRewardSelectionState(Context, Flow));
return;
}
Flow.Scheduler.ChangeState(new CombatFinishFormState(Context, Flow));
Flow.ChangeState(new CombatFinishFormState(Context, Flow));
}
}
}
@@ -24,7 +24,7 @@ namespace GeometryTD.CustomComponent
if (Flow.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory))
{
Flow.Scheduler.ChangeState(new CombatSettlementState(Context, Flow, reason, isVictory));
Flow.ChangeState(new CombatSettlementState(Context, Flow, reason, isVictory));
return;
}
@@ -43,4 +43,4 @@ namespace GeometryTD.CustomComponent
Flow.CompleteCurrentPhase();
}
}
}
}
@@ -0,0 +1,21 @@
using GeometryTD.DataTable;
namespace GeometryTD.CustomComponent
{
internal interface ICombatSchedulerHost
{
DRLevel CurrentLevel { get; }
int DisplayPhaseIndex { get; }
int PhaseCount { get; }
int CurrentCoin { get; }
int CurrentBaseHp { get; }
bool CanEndCombat { get; }
void ChangeState(CombatStateBase nextState);
bool TryConsumeCoin(int coin);
void AddCoin(int coin);
bool TryEndCombatByPlayer();
bool TryDebugFail(string errorMessage);
bool OnCombatFinishReturnRequested();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ffb072614ac46464a89a01c0a2bade49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -7,7 +7,7 @@ namespace GeometryTD.UI
{
public class CombatFinishFormUseCase : IUIUseCase
{
private CombatScheduler _combatScheduler;
private ICombatSchedulerHost _combatSchedulerHost;
private CombatSettlementContext _settlementContext;
private bool _isSummaryPrepared;
@@ -27,14 +27,14 @@ namespace GeometryTD.UI
_isSummaryPrepared = true;
}
public CombatFinishFormUseCase(CombatScheduler combatScheduler)
internal CombatFinishFormUseCase(ICombatSchedulerHost combatSchedulerHost)
{
this._combatScheduler = combatScheduler;
_combatSchedulerHost = combatSchedulerHost;
}
public bool TryReturnToMenu()
{
return _combatScheduler.OnCombatFinishReturnRequested();
return _combatSchedulerHost != null && _combatSchedulerHost.OnCombatFinishReturnRequested();
}
private CombatFinishFormRawData BuildModel()
@@ -44,7 +44,7 @@ namespace GeometryTD.UI
_settlementContext = null;
}
BackpackInventoryData rewardInventory = _settlementContext?.RewardInventory;
BackpackInventoryData rewardInventory = _settlementContext?.Summary.RewardInventory;
if (rewardInventory == null)
{
rewardInventory = InventorySeedUtility.CreateSampleInventory();
@@ -52,8 +52,8 @@ namespace GeometryTD.UI
return new CombatFinishFormRawData
{
DefeatedEnemyCount = Mathf.Max(0, _settlementContext?.DefeatedEnemyCount ?? 0),
GainedGold = Mathf.Max(0, _settlementContext?.GainedGold ?? 0),
DefeatedEnemyCount = Mathf.Max(0, _settlementContext?.Summary.DefeatedEnemyCount ?? 0),
GainedGold = Mathf.Max(0, _settlementContext?.Summary.GainedGold ?? 0),
RewardInventory = rewardInventory,
CanReturn = true
};