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:
SepComet 2026-03-07 20:00:39 +08:00
parent b38088c3ea
commit 3ad7d04b47
15 changed files with 224 additions and 121 deletions

View File

@ -45,7 +45,7 @@ namespace GeometryTD.CustomComponent
_currentMap = null; _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; errorMessage = null;
if (_entity == null) if (_entity == null)
@ -59,7 +59,7 @@ namespace GeometryTD.CustomComponent
return false; return false;
} }
if (!TryOpenCombatInfoForm(scheduler, out errorMessage)) if (!TryOpenCombatInfoForm(schedulerHost, out errorMessage))
{ {
return false; return false;
} }
@ -253,7 +253,7 @@ namespace GeometryTD.CustomComponent
return true; return true;
} }
private bool TryOpenCombatInfoForm(CombatScheduler scheduler, out string errorMessage) private bool TryOpenCombatInfoForm(ICombatSchedulerHost schedulerHost, out string errorMessage)
{ {
errorMessage = null; errorMessage = null;
if (_combatInfoFormUseCase == null) if (_combatInfoFormUseCase == null)
@ -263,9 +263,9 @@ namespace GeometryTD.CustomComponent
} }
_combatInfoFormUseCase.Configure( _combatInfoFormUseCase.Configure(
() => BuildCombatInfoFormRawData(scheduler), () => BuildCombatInfoFormRawData(schedulerHost),
() => scheduler != null && scheduler.CanEndCombat && scheduler.TryEndCombatByPlayer(), () => schedulerHost != null && schedulerHost.CanEndCombat && schedulerHost.TryEndCombatByPlayer(),
() => scheduler != null && scheduler.TryDebugFail("Manual debug fail from CombatInfoForm.")); () => schedulerHost != null && schedulerHost.TryDebugFail("Manual debug fail from CombatInfoForm."));
int? serialId = GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm); int? serialId = GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
if (!serialId.HasValue) if (!serialId.HasValue)
@ -279,26 +279,26 @@ namespace GeometryTD.CustomComponent
return true; return true;
} }
private static CombatInfoFormRawData BuildCombatInfoFormRawData(CombatScheduler scheduler) private static CombatInfoFormRawData BuildCombatInfoFormRawData(ICombatSchedulerHost schedulerHost)
{ {
if (scheduler == null) if (schedulerHost == null)
{ {
return null; return null;
} }
DRLevel level = scheduler.CurrentLevel; DRLevel level = schedulerHost.CurrentLevel;
LevelThemeType themeType = level != null ? level.LevelThemeType : LevelThemeType.None; LevelThemeType themeType = level != null ? level.LevelThemeType : LevelThemeType.None;
int levelId = level != null ? level.Id : 0; int levelId = level != null ? level.Id : 0;
int baseHpMax = level != null ? Mathf.Max(0, level.BaseHp) : 0; int baseHpMax = level != null ? Mathf.Max(0, level.BaseHp) : 0;
return CombatInfoFormUseCase.BuildRawData( return CombatInfoFormUseCase.BuildRawData(
themeType, themeType,
levelId, levelId,
scheduler.DisplayPhaseIndex, schedulerHost.DisplayPhaseIndex,
scheduler.PhaseCount, schedulerHost.PhaseCount,
scheduler.CurrentCoin, schedulerHost.CurrentCoin,
scheduler.CurrentBaseHp, schedulerHost.CurrentBaseHp,
baseHpMax, baseHpMax,
scheduler.CanEndCombat); schedulerHost.CanEndCombat);
} }
private void CloseCombatInfoForm() private void CloseCombatInfoForm()

View File

@ -9,7 +9,7 @@ using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
public partial class CombatScheduler public partial class CombatScheduler : ICombatSchedulerHost
{ {
private readonly CombatSchedulerRuntimeContext _context = new(); private readonly CombatSchedulerRuntimeContext _context = new();
private readonly CombatSchedulerFlowCoordinator _flowCoordinator; private readonly CombatSchedulerFlowCoordinator _flowCoordinator;
@ -237,6 +237,11 @@ namespace GeometryTD.CustomComponent
return _context.CurrentState is CombatFailedState; return _context.CurrentState is CombatFailedState;
} }
void ICombatSchedulerHost.ChangeState(CombatStateBase nextState)
{
ChangeState(nextState);
}
internal void ChangeState(CombatStateBase nextState) internal void ChangeState(CombatStateBase nextState)
{ {
if (ReferenceEquals(_context.CurrentState, nextState)) if (ReferenceEquals(_context.CurrentState, nextState))

View File

@ -10,17 +10,21 @@ namespace GeometryTD.CustomComponent
{ {
internal sealed class CombatSchedulerFlowCoordinator internal sealed class CombatSchedulerFlowCoordinator
{ {
private readonly CombatScheduler _scheduler; private readonly ICombatSchedulerHost _schedulerHost;
private readonly CombatSchedulerRuntimeContext _context; private readonly CombatSchedulerRuntimeContext _context;
public ICombatSchedulerHost Host => _schedulerHost;
public CombatScheduler Scheduler => _scheduler; public CombatSchedulerFlowCoordinator(ICombatSchedulerHost schedulerHost, CombatSchedulerRuntimeContext context)
public CombatSchedulerFlowCoordinator(CombatScheduler scheduler, CombatSchedulerRuntimeContext context)
{ {
_scheduler = scheduler; _schedulerHost = schedulerHost;
_context = context; _context = context;
} }
public void ChangeState(CombatStateBase nextState)
{
_schedulerHost.ChangeState(nextState);
}
public int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount) public int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount)
{ {
if (displayPhaseIndex <= 0 || phaseCount <= 0) if (displayPhaseIndex <= 0 || phaseCount <= 0)
@ -60,7 +64,7 @@ namespace GeometryTD.CustomComponent
public void EnsureCombatFinishFormUseCaseBound() public void EnsureCombatFinishFormUseCaseBound()
{ {
_context.CombatFinishFormUseCase ??= new CombatFinishFormUseCase(_scheduler); _context.CombatFinishFormUseCase ??= new CombatFinishFormUseCase(_schedulerHost);
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _context.CombatFinishFormUseCase); GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _context.CombatFinishFormUseCase);
} }
@ -88,18 +92,18 @@ namespace GeometryTD.CustomComponent
{ {
if (!_context.PhaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase)) 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; return false;
} }
_context.SpawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries); _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; return true;
} }
public void EnterWaitingForPhaseEnd() public void EnterWaitingForPhaseEnd()
{ {
_scheduler.ChangeState(new CombatWaitingForPhaseEndState(_context, this)); _schedulerHost.ChangeState(new CombatWaitingForPhaseEndState(_context, this));
} }
public void CompleteCurrentPhase() public void CompleteCurrentPhase()
@ -143,7 +147,7 @@ namespace GeometryTD.CustomComponent
} }
_context.SettlementFlowService.ApplySelectedReward(_context.SettlementContext, selectedReward); _context.SettlementFlowService.ApplySelectedReward(_context.SettlementContext, selectedReward);
_scheduler.ChangeState(new CombatFinishFormState(_context, this)); _schedulerHost.ChangeState(new CombatFinishFormState(_context, this));
} }
public void OnFullBaseHpRewardGiveUp() public void OnFullBaseHpRewardGiveUp()
@ -153,7 +157,7 @@ namespace GeometryTD.CustomComponent
return; return;
} }
_scheduler.ChangeState(new CombatFinishFormState(_context, this)); _schedulerHost.ChangeState(new CombatFinishFormState(_context, this));
} }
public LevelThemeType ResolveCurrentThemeType() public LevelThemeType ResolveCurrentThemeType()
@ -176,6 +180,16 @@ namespace GeometryTD.CustomComponent
return _context.CombatInRunResourceManager.ApplyBaseDamage(damage); 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() public int GetCurrentBaseHp()
{ {
return Mathf.Max(0, _context.CombatInRunResourceManager.CurrentBaseHp); return Mathf.Max(0, _context.CombatInRunResourceManager.CurrentBaseHp);
@ -231,7 +245,7 @@ namespace GeometryTD.CustomComponent
return; return;
} }
_scheduler.ChangeState(new CombatFailedState(_context, this, errorMessage)); _schedulerHost.ChangeState(new CombatFailedState(_context, this, errorMessage));
} }
public void OnCombatFailureDialogConfirmed(object userData) public void OnCombatFailureDialogConfirmed(object userData)

View File

@ -3,6 +3,20 @@ using GeometryTD.Definition;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
internal sealed class CombatSettlementContext 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 bool IsVictory;
public int FinalCoin; public int FinalCoin;
@ -11,12 +25,21 @@ namespace GeometryTD.CustomComponent
public int DefeatedEnemyCount; public int DefeatedEnemyCount;
public int GainedGold; public int GainedGold;
public BackpackInventoryData RewardInventory; public BackpackInventoryData RewardInventory;
public bool ShouldOpenRewardSelection; public string Reason;
public bool DidEnterRewardSelection; public CombatSettlementPenaltyResult Penalty { get; } = new();
}
internal sealed class CombatSettlementPenaltyResult
{
public bool ShouldApplyLowBaseHpPenalty; public bool ShouldApplyLowBaseHpPenalty;
public float LowBaseHpEndurancePenaltyValue; public float LowBaseHpEndurancePenaltyValue;
public int AffectedTowerCount; public int AffectedTowerCount;
public bool IsCommitted; }
public string Reason;
internal sealed class CombatSettlementSummary
{
public int DefeatedEnemyCount;
public int GainedGold;
public BackpackInventoryData RewardInventory;
} }
} }

View File

@ -39,22 +39,25 @@ namespace GeometryTD.CustomComponent
CombatSettlementContext settlementContext = new CombatSettlementContext 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( Log.Info(
"Combat settlement resolved. Level={0}, Reason={1}, BaseHp={2}/{3}, LevelReward={4}, BonusRate={5:P0}, BonusGold={6}, FullHpRewardSelect={7}, LowHpPenalty={8}.", "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) public void CommitSettlementInventory(CombatSettlementContext settlementContext)
{ {
if (settlementContext == null || settlementContext.IsCommitted) if (settlementContext == null || settlementContext.Flow.IsCommitted)
{ {
return; return;
} }
BackpackInventoryData rewardInventory = settlementContext.RewardInventory ?? new BackpackInventoryData(); BackpackInventoryData rewardInventory = settlementContext.Result.RewardInventory ?? new BackpackInventoryData();
GameEntry.PlayerInventory?.MergeInventory(rewardInventory); GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
settlementContext.RewardInventory = rewardInventory; settlementContext.Result.RewardInventory = rewardInventory;
settlementContext.AffectedTowerCount = ApplyDeferredSettlementPenalty(settlementContext); settlementContext.Summary.RewardInventory = rewardInventory;
settlementContext.IsCommitted = true; settlementContext.Result.Penalty.AffectedTowerCount = ApplyDeferredSettlementPenalty(settlementContext);
settlementContext.Flow.IsCommitted = true;
} }
public bool TryPrepareRewardSelection( public bool TryPrepareRewardSelection(
@ -104,7 +108,7 @@ namespace GeometryTD.CustomComponent
RewardSelectDisplayCount); RewardSelectDisplayCount);
if (candidateItems == null || candidateItems.Count <= 0) if (candidateItems == null || candidateItems.Count <= 0)
{ {
settlementContext.ShouldOpenRewardSelection = false; settlementContext.Flow.ShouldOpenRewardSelection = false;
return false; return false;
} }
@ -122,7 +126,7 @@ namespace GeometryTD.CustomComponent
if (rewardPool.Count <= 0) if (rewardPool.Count <= 0)
{ {
settlementContext.ShouldOpenRewardSelection = false; settlementContext.Flow.ShouldOpenRewardSelection = false;
return false; return false;
} }
@ -138,23 +142,24 @@ namespace GeometryTD.CustomComponent
RewardSelectFormRawData rawData = rewardSelectFormUseCase.CreateInitialModel(); RewardSelectFormRawData rawData = rewardSelectFormUseCase.CreateInitialModel();
if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0) if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0)
{ {
settlementContext.ShouldOpenRewardSelection = false; settlementContext.Flow.ShouldOpenRewardSelection = false;
return false; return false;
} }
settlementContext.DidEnterRewardSelection = true; settlementContext.Flow.DidEnterRewardSelection = true;
GameEntry.UIRouter.OpenUI(UIFormType.RewardSelectForm, rawData); GameEntry.UIRouter.OpenUI(UIFormType.RewardSelectForm, rawData);
return true; return true;
} }
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward) public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
{ {
if (settlementContext?.RewardInventory == null || selectedReward?.SourceItem == null) if (settlementContext?.Result.RewardInventory == null || selectedReward?.SourceItem == null)
{ {
return; return;
} }
TryAppendRewardComponent(settlementContext.RewardInventory, selectedReward.SourceItem); TryAppendRewardComponent(settlementContext.Result.RewardInventory, selectedReward.SourceItem);
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
} }
public void OpenCombatFinishForm( public void OpenCombatFinishForm(
@ -227,8 +232,8 @@ namespace GeometryTD.CustomComponent
private static int ApplyDeferredSettlementPenalty(CombatSettlementContext settlementContext) private static int ApplyDeferredSettlementPenalty(CombatSettlementContext settlementContext)
{ {
if (settlementContext == null || if (settlementContext == null ||
!settlementContext.ShouldApplyLowBaseHpPenalty || !settlementContext.Result.Penalty.ShouldApplyLowBaseHpPenalty ||
settlementContext.LowBaseHpEndurancePenaltyValue <= 0f) settlementContext.Result.Penalty.LowBaseHpEndurancePenaltyValue <= 0f)
{ {
return 0; return 0;
} }
@ -239,7 +244,7 @@ namespace GeometryTD.CustomComponent
return 0; return 0;
} }
return inventory.ReduceAllTowerEndurance(settlementContext.LowBaseHpEndurancePenaltyValue); return inventory.ReduceAllTowerEndurance(settlementContext.Result.Penalty.LowBaseHpEndurancePenaltyValue);
} }
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem) private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)

View File

@ -20,7 +20,7 @@ namespace GeometryTD.CustomComponent
Context.SettlementFlowService.OpenCombatFinishForm( Context.SettlementFlowService.OpenCombatFinishForm(
Context.SettlementContext, Context.SettlementContext,
Context.CombatFinishFormUseCase); Context.CombatFinishFormUseCase);
Flow.Scheduler.ChangeState(new CombatWaitingForReturnState(Context, Flow)); Flow.ChangeState(new CombatWaitingForReturnState(Context, Flow));
} }
} }
} }

View File

@ -22,7 +22,7 @@ namespace GeometryTD.CustomComponent
} }
MapEntityLoadContext mapLoadContext = BuildMapLoadContext(); 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}"); Flow.EnterFailureFallback($"Combat loading failed. {errorMessage}");
} }
@ -66,7 +66,7 @@ namespace GeometryTD.CustomComponent
participantTowerSnapshot: GameEntry.PlayerInventory != null participantTowerSnapshot: GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetParticipantTowerSnapshot() ? GameEntry.PlayerInventory.GetParticipantTowerSnapshot()
: null); : null);
return new MapEntityLoadContext(mapData, Flow.Scheduler.TryConsumeCoin, Flow.Scheduler.AddCoin); return new MapEntityLoadContext(mapData, Flow.TryConsumeCoin, Flow.AddCoin);
} }
} }
} }

View File

@ -25,7 +25,7 @@ namespace GeometryTD.CustomComponent
Flow.OnFullBaseHpRewardSelected, Flow.OnFullBaseHpRewardSelected,
Flow.OnFullBaseHpRewardGiveUp)) Flow.OnFullBaseHpRewardGiveUp))
{ {
Flow.Scheduler.ChangeState(new CombatFinishFormState(Context, Flow)); Flow.ChangeState(new CombatFinishFormState(Context, Flow));
} }
} }

View File

@ -62,7 +62,7 @@ namespace GeometryTD.CustomComponent
if (Flow.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory)) 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; return;
} }

View File

@ -26,13 +26,13 @@ namespace GeometryTD.CustomComponent
Context.CurrentLevel, Context.CurrentLevel,
Context.EnemyManager.DefeatedEnemyCount, Context.EnemyManager.DefeatedEnemyCount,
Context.CombatInRunResourceManager); 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; return;
} }
Flow.Scheduler.ChangeState(new CombatFinishFormState(Context, Flow)); Flow.ChangeState(new CombatFinishFormState(Context, Flow));
} }
} }
} }

View File

@ -24,7 +24,7 @@ namespace GeometryTD.CustomComponent
if (Flow.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory)) 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; return;
} }

View File

@ -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();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ffb072614ac46464a89a01c0a2bade49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -7,7 +7,7 @@ namespace GeometryTD.UI
{ {
public class CombatFinishFormUseCase : IUIUseCase public class CombatFinishFormUseCase : IUIUseCase
{ {
private CombatScheduler _combatScheduler; private ICombatSchedulerHost _combatSchedulerHost;
private CombatSettlementContext _settlementContext; private CombatSettlementContext _settlementContext;
private bool _isSummaryPrepared; private bool _isSummaryPrepared;
@ -27,14 +27,14 @@ namespace GeometryTD.UI
_isSummaryPrepared = true; _isSummaryPrepared = true;
} }
public CombatFinishFormUseCase(CombatScheduler combatScheduler) internal CombatFinishFormUseCase(ICombatSchedulerHost combatSchedulerHost)
{ {
this._combatScheduler = combatScheduler; _combatSchedulerHost = combatSchedulerHost;
} }
public bool TryReturnToMenu() public bool TryReturnToMenu()
{ {
return _combatScheduler.OnCombatFinishReturnRequested(); return _combatSchedulerHost != null && _combatSchedulerHost.OnCombatFinishReturnRequested();
} }
private CombatFinishFormRawData BuildModel() private CombatFinishFormRawData BuildModel()
@ -44,7 +44,7 @@ namespace GeometryTD.UI
_settlementContext = null; _settlementContext = null;
} }
BackpackInventoryData rewardInventory = _settlementContext?.RewardInventory; BackpackInventoryData rewardInventory = _settlementContext?.Summary.RewardInventory;
if (rewardInventory == null) if (rewardInventory == null)
{ {
rewardInventory = InventorySeedUtility.CreateSampleInventory(); rewardInventory = InventorySeedUtility.CreateSampleInventory();
@ -52,8 +52,8 @@ namespace GeometryTD.UI
return new CombatFinishFormRawData return new CombatFinishFormRawData
{ {
DefeatedEnemyCount = Mathf.Max(0, _settlementContext?.DefeatedEnemyCount ?? 0), DefeatedEnemyCount = Mathf.Max(0, _settlementContext?.Summary.DefeatedEnemyCount ?? 0),
GainedGold = Mathf.Max(0, _settlementContext?.GainedGold ?? 0), GainedGold = Mathf.Max(0, _settlementContext?.Summary.GainedGold ?? 0),
RewardInventory = rewardInventory, RewardInventory = rewardInventory,
CanReturn = true CanReturn = true
}; };

View File

@ -6,7 +6,7 @@
`docs/CombatNodeArchitecture.md` 继续收敛 `CombatNode` 域职责。当前骨架已经基本到位,后续重点是: `docs/CombatNodeArchitecture.md` 继续收敛 `CombatNode` 域职责。当前骨架已经基本到位,后续重点是:
- 继续保持 `CombatScheduler` 作为唯一状态机边界,避免把新业务重新堆回本体。 - 继续保持 `CombatScheduler` 作为唯一状态机边界,避免把新业务重新堆回本体。
- 继续完成 `MapData + Event` 解耦收尾,确认 `MapEntity` 不再反查 `CombatNode` 域运行时。 - `MapData + Event` 已收口的基础上,继续保持 `MapEntity`反查 `CombatNode` 域运行时。
- 稳定 `CombatSettlementContext` 的模型边界,避免流程控制字段和展示摘要继续混杂增长。 - 稳定 `CombatSettlementContext` 的模型边界,避免流程控制字段和展示摘要继续混杂增长。
- 补 Unity 编译、PlayMode 和失败路径回归验证,把这轮结构调整真正跑通。 - 补 Unity 编译、PlayMode 和失败路径回归验证,把这轮结构调整真正跑通。
@ -195,51 +195,76 @@
- `Assets/GameMain/Scripts/UI/Combat/UseCase/CombatInfoFormUseCase.cs` - `Assets/GameMain/Scripts/UI/Combat/UseCase/CombatInfoFormUseCase.cs`
- `Assets/GameMain/Scripts/Event/Combat/CombatDebugFailEventArgs.cs` - `Assets/GameMain/Scripts/Event/Combat/CombatDebugFailEventArgs.cs`
### 10. MapData + Event 解耦已完成一轮收口
- `MapData` 已收口为纯初始化快照,不再承载 coin 写接口委托
- 已新增 `MapEntityLoadContext`
- 用于把 `MapData` 快照与 coin 命令通道拆开传给地图加载
- `CombatLoadingState` 现在会组装:
- `MapData`
- `MapEntityLoadContext`
- `CombatLoadSession` / `EntityExtension.ShowMap(...)` 已切到 `MapEntityLoadContext`
- `MapEntity` 当前通过:
- `MapEntityLoadContext` 获取初始快照与 coin 命令通道
- `CombatCoinChangedEventArgs` 同步后续 coin 变化
- 已新增 `MapCombatRuntimeBridge`
- 收口地图侧 coin 当前值、命令调用与事件订阅
- `MapEntity` 不再自己维护 `_currentCoin` 和 coin 事件订阅样板
当前结论:
- 地图侧已经完成“`MapData` 初始快照 + Event 同步 + 独立命令桥接”的接线
- 当前未发现 `MapEntity` / 地图侧服务对 `CombatNodeComponent``CombatScheduler` 的运行时反查
关键文件:
- `Assets/GameMain/Scripts/Entity/EntityData/MapData.cs`
- `Assets/GameMain/Scripts/Entity/EntityData/MapEntityLoadContext.cs`
- `Assets/GameMain/Scripts/Scene/Map/MapCombatRuntimeBridge.cs`
- `Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs`
- `Assets/GameMain/Scripts/Entity/EntityExtension.cs`
### 11. FlowCoordinator 已切到极小宿主接口
- 已新增 `ICombatSchedulerHost`
- `CombatSchedulerFlowCoordinator` 不再直接依赖 `CombatScheduler` 具体类,而是只依赖:
- 状态切换入口
- coin 命令转发
- CombatInfo / FinishForm 所需的只读查询与回调
- `CombatLoadSession``CombatFinishFormUseCase` 也已切到 `ICombatSchedulerHost`
- 状态类当前通过 `Flow.ChangeState(...)``Flow` 上的轻量转发访问宿主,不再持有 `CombatScheduler` 具体类型引用
关键文件:
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerHost.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs`
- `Assets/GameMain/Scripts/UI/Combat/UseCase/CombatFinishFormUseCase.cs`
### 12. CombatSettlementContext 已拆成 Flow / Result / Summary 分层
- `CombatSettlementContext` 当前明确区分:
- `Flow`
- `Result`
- `Summary`
- 流程控制字段现在集中在 `Flow`
- 结算事实与惩罚事实现在集中在 `Result`
- `CombatFinishFormUseCase` 当前只消费 `Summary`
- 奖励选择与延迟提交惩罚已同步切到分层后的上下文访问路径
关键文件:
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementFlowService.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs`
- `Assets/GameMain/Scripts/UI/Combat/UseCase/CombatFinishFormUseCase.cs`
## 还没完成 ## 还没完成
### 1. MapData + Event 解耦还没完全收口 ### 1. 需要补实际运行验证
当前:
- `CombatLoadingState` 已经会组装更完整的 `MapData`
- `MapEntity` 已经主要从 `MapData` 取初始上下文
- 但还需要继续确认地图侧事件和运行时依赖是否已经完全摆脱对 `CombatNode` 域的反查
目标状态:
- `CombatLoadingState``CombatInRunResourceManager` 读取快照
- `CombatLoadingState` 组装更完整的 `MapData`
- `MapEntity` 后续通过 `MapData + Event` 获取上下文,而不是反查 `CombatNode`
### 2. CombatScheduler 本体仍然还有收紧空间
当前:
- 生命周期入口、状态切换入口、对外公开接口、事件桥回调入口仍在 `CombatScheduler.cs`
- 共享字段和共享流程已经拆到了 `RuntimeContext + FlowCoordinator`
- 但 `FlowCoordinator` 目前仍直接依赖 `CombatScheduler` 宿主对象
目标状态:
- 对外公开接口与内部宿主职责进一步分离
- 若继续收紧,可评估是否引入极小宿主接口,只暴露:
- `ChangeState(...)`
- 必要的回调/转发入口
- 继续避免把新业务重新塞回 `CombatScheduler.cs`
### 3. CombatSettlementContext 还可以继续整理字段分层
当前:
- `CombatSettlementContext` 已能支撑结束链
- 但字段仍是平铺增长
目标状态:
- 更明确地区分:
- 流程控制字段
- 结算事实字段
- 展示摘要字段
- 避免后续继续把 UI 需求和流程控制混在一起
### 4. 需要补实际运行验证
当前改动已覆盖: 当前改动已覆盖:
- 状态机结构 - 状态机结构
- 结束链/失败链协议 - 结束链/失败链协议
- 手动失败测试入口 - 手动失败测试入口
- `CombatSchedulerRuntimeContext + CombatSchedulerFlowCoordinator` - `CombatSchedulerRuntimeContext + CombatSchedulerFlowCoordinator + ICombatSchedulerHost`
- `CombatSettlementContext` 与延迟提交惩罚 - `CombatSettlementContext` 分层与延迟提交惩罚
- `MapData + Event + MapCombatRuntimeBridge`
但仍缺: 但仍缺:
- Unity 编译验证 - Unity 编译验证
@ -249,10 +274,9 @@
## 推荐的后续执行顺序 ## 推荐的后续执行顺序
1. 做 `MapData + Event` 解耦收尾,排查 `MapEntity` 和地图侧事件是否还反查 `CombatNode` 1. 补 Unity 编译与手动回归验证
2. 评估是否给 `FlowCoordinator` 引入极小宿主接口,继续收紧 `CombatScheduler` 本体 2. 重点验证正常结束链、异常失败链、新开局无残留状态
3. 继续整理 `CombatSettlementContext` 的字段分层 3. 若验证中暴露边界问题,再继续做小步收口
4. 补 Unity 编译与手动回归验证
## 当前做变更时要记住的约束 ## 当前做变更时要记住的约束