A1 + A2
This commit is contained in:
parent
777c58b812
commit
c76fd85a2c
|
|
@ -177,11 +177,7 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
public void StartCombat(
|
||||
int levelId = 0,
|
||||
string runId = null,
|
||||
int runSeed = 0,
|
||||
int nodeId = 0,
|
||||
RunNodeType nodeType = RunNodeType.None,
|
||||
int sequenceIndex = -1)
|
||||
RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!EnsureCombatRuntimeInitialized())
|
||||
{
|
||||
|
|
@ -232,11 +228,7 @@ namespace GeometryTD.CustomComponent
|
|||
selectedLevel,
|
||||
phaseList,
|
||||
_selectedSpawnEntriesByPhaseId,
|
||||
runId,
|
||||
runSeed,
|
||||
nodeId,
|
||||
nodeType,
|
||||
sequenceIndex))
|
||||
context))
|
||||
{
|
||||
CurrentLevel = null;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -69,11 +69,7 @@ namespace GeometryTD.CustomComponent
|
|||
DRLevel level,
|
||||
IReadOnlyList<DRLevelPhase> phases,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId,
|
||||
string runId = null,
|
||||
int runSeed = 0,
|
||||
int nodeId = 0,
|
||||
RunNodeType nodeType = RunNodeType.None,
|
||||
int sequenceIndex = -1)
|
||||
RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized || _runtime.Entity == null)
|
||||
{
|
||||
|
|
@ -95,11 +91,7 @@ namespace GeometryTD.CustomComponent
|
|||
_runtime.DidCombatWin = true;
|
||||
|
||||
_runtime.CurrentLevel = level;
|
||||
_runtime.RunId = runId;
|
||||
_runtime.RunSeed = runSeed;
|
||||
_runtime.NodeId = nodeId;
|
||||
_runtime.NodeType = nodeType;
|
||||
_runtime.SequenceIndex = sequenceIndex;
|
||||
_runtime.NodeContext = context != null ? context.Clone() : null;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
_runtime.CombatRunResourceStore.InitializeForCombat(level);
|
||||
|
|
@ -205,8 +197,8 @@ namespace GeometryTD.CustomComponent
|
|||
int nextDropOrdinal = _runtime.NextDropOrdinal;
|
||||
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(
|
||||
context,
|
||||
_runtime.RunSeed,
|
||||
_runtime.SequenceIndex,
|
||||
_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);
|
||||
|
|
|
|||
|
|
@ -55,11 +55,7 @@ namespace GeometryTD.CustomComponent
|
|||
_runtime.DidCombatWin = true;
|
||||
_runtime.IsCompleted = false;
|
||||
_runtime.NodeEnterFired = false;
|
||||
_runtime.RunId = null;
|
||||
_runtime.RunSeed = 0;
|
||||
_runtime.NodeId = 0;
|
||||
_runtime.NodeType = RunNodeType.None;
|
||||
_runtime.SequenceIndex = -1;
|
||||
_runtime.NodeContext = null;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
}
|
||||
|
|
@ -203,16 +199,20 @@ namespace GeometryTD.CustomComponent
|
|||
public void CompleteNormalCombatAndNotify(bool didCombatWin)
|
||||
{
|
||||
CompleteCombat(didCombatWin);
|
||||
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_runtime.RunId,
|
||||
_runtime.NodeId,
|
||||
_runtime.NodeType,
|
||||
_runtime.SequenceIndex,
|
||||
nodeContext?.RunId,
|
||||
nodeContext?.NodeId ?? 0,
|
||||
nodeContext?.NodeType ?? RunNodeType.None,
|
||||
nodeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
didCombatWin,
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
|
||||
nodeContext != null
|
||||
? nodeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
}
|
||||
|
||||
public void CompleteFailureCombatAndNotify()
|
||||
|
|
@ -222,16 +222,20 @@ namespace GeometryTD.CustomComponent
|
|||
CloseRewardSelectForm();
|
||||
CloseDialogForm();
|
||||
CompleteCombat(false);
|
||||
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_runtime.RunId,
|
||||
_runtime.NodeId,
|
||||
_runtime.NodeType,
|
||||
_runtime.SequenceIndex,
|
||||
nodeContext?.RunId,
|
||||
nodeContext?.NodeId ?? 0,
|
||||
nodeContext?.NodeType ?? RunNodeType.None,
|
||||
nodeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Exception,
|
||||
false,
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
|
||||
nodeContext != null
|
||||
? nodeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
}
|
||||
|
||||
public bool HandleStartFailure(string errorMessage)
|
||||
|
|
|
|||
|
|
@ -29,11 +29,7 @@ namespace GeometryTD.CustomComponent
|
|||
public bool IsCompleted { get; set; }
|
||||
public bool NodeEnterFired { get; set; }
|
||||
public CombatSettlementContext SettlementContext { get; set; }
|
||||
public string RunId { get; set; }
|
||||
public int RunSeed { get; set; }
|
||||
public int NodeId { get; set; }
|
||||
public RunNodeType NodeType { get; set; }
|
||||
public int SequenceIndex { get; set; }
|
||||
public RunNodeExecutionContext NodeContext { get; set; }
|
||||
public int NextDropOrdinal { get; set; }
|
||||
public int NextRewardOrdinal { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ namespace GeometryTD.CustomComponent
|
|||
Runtime.SettlementContext,
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Coordinator.ResolveCurrentThemeType(),
|
||||
Runtime.RunSeed,
|
||||
Runtime.SequenceIndex,
|
||||
Runtime.NodeContext != null ? Runtime.NodeContext.RunSeed : 0,
|
||||
Runtime.NodeContext != null ? Runtime.NodeContext.SequenceIndex : -1,
|
||||
ref nextRewardOrdinal,
|
||||
Runtime.RewardSelectFormUseCase,
|
||||
Coordinator.OnFullBaseHpRewardSelected,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
|
|
@ -41,10 +42,10 @@ namespace GeometryTD.CustomComponent
|
|||
GameEntry.Event.Fire(
|
||||
Coordinator,
|
||||
NodeEnterEventArgs.Create(
|
||||
Runtime.RunId,
|
||||
Runtime.NodeId,
|
||||
Runtime.NodeType,
|
||||
Runtime.SequenceIndex));
|
||||
Runtime.NodeContext?.RunId,
|
||||
Runtime.NodeContext?.NodeId ?? 0,
|
||||
Runtime.NodeContext?.NodeType ?? RunNodeType.None,
|
||||
Runtime.NodeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ namespace GeometryTD.CustomComponent
|
|||
{
|
||||
public class EventNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private string _activeRunId;
|
||||
private int _activeNodeId;
|
||||
private RunNodeType _activeNodeType = RunNodeType.None;
|
||||
private int _activeSequenceIndex = -1;
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
|
||||
private readonly List<EventItem> _eventItems = new List<EventItem>();
|
||||
|
||||
|
|
@ -54,7 +51,7 @@ namespace GeometryTD.CustomComponent
|
|||
Log.Info("EventNodeComponent initialized with {0} events.", _eventItems.Count);
|
||||
}
|
||||
|
||||
public void StartEvent(string runId = null, int nodeId = 0, RunNodeType nodeType = RunNodeType.None, int sequenceIndex = -1)
|
||||
public void StartEvent(RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
|
|
@ -76,13 +73,16 @@ namespace GeometryTD.CustomComponent
|
|||
return;
|
||||
}
|
||||
|
||||
_activeRunId = runId;
|
||||
_activeNodeId = nodeId;
|
||||
_activeNodeType = nodeType;
|
||||
_activeSequenceIndex = sequenceIndex;
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
_eventFormUseCase.SetCurrentEvent(randomEvent);
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.EventForm);
|
||||
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create(runId, nodeId, nodeType, sequenceIndex));
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeEnterEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
public void EndEvent()
|
||||
|
|
@ -91,22 +91,22 @@ namespace GeometryTD.CustomComponent
|
|||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_activeRunId,
|
||||
_activeNodeId,
|
||||
_activeNodeType,
|
||||
_activeSequenceIndex,
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
|
||||
_activeContext != null
|
||||
? _activeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
ClearActiveNodeContext();
|
||||
}
|
||||
|
||||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeRunId = null;
|
||||
_activeNodeId = 0;
|
||||
_activeNodeType = RunNodeType.None;
|
||||
_activeSequenceIndex = -1;
|
||||
_activeContext = null;
|
||||
}
|
||||
|
||||
private static EventOption[] ParseOptions(string optionsRaw)
|
||||
|
|
|
|||
|
|
@ -171,9 +171,9 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
|
||||
float totalWeight = 0f;
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
foreach (var weight in _rarityWeightBuffer)
|
||||
{
|
||||
totalWeight += Mathf.Max(0f, _rarityWeightBuffer[rarityIndex]);
|
||||
totalWeight += Mathf.Max(0f, weight);
|
||||
}
|
||||
|
||||
if (totalWeight <= 0f)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,7 @@ namespace GeometryTD.CustomComponent
|
|||
{
|
||||
public class ShopNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private string _activeRunId;
|
||||
private int _activeRunSeed;
|
||||
private int _activeNodeId;
|
||||
private RunNodeType _activeNodeType = RunNodeType.None;
|
||||
private int _activeSequenceIndex = -1;
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
|
||||
private ShopFormUseCase _shopFormUseCase;
|
||||
private bool _initialized;
|
||||
|
|
@ -29,26 +25,30 @@ namespace GeometryTD.CustomComponent
|
|||
_initialized = true;
|
||||
}
|
||||
|
||||
public void StartShop(string runId = null, int runSeed = 0, int nodeId = 0, RunNodeType nodeType = RunNodeType.None, int sequenceIndex = -1)
|
||||
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;
|
||||
}
|
||||
|
||||
_activeRunId = runId;
|
||||
_activeRunSeed = runSeed;
|
||||
_activeNodeId = nodeId;
|
||||
_activeNodeType = nodeType;
|
||||
_activeSequenceIndex = sequenceIndex;
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.ShopForm);
|
||||
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create(runId, nodeId, nodeType, sequenceIndex));
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeEnterEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
public void EndShop()
|
||||
|
|
@ -57,23 +57,22 @@ namespace GeometryTD.CustomComponent
|
|||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_activeRunId,
|
||||
_activeNodeId,
|
||||
_activeNodeType,
|
||||
_activeSequenceIndex,
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
|
||||
_activeContext != null
|
||||
? _activeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
ClearActiveNodeContext();
|
||||
}
|
||||
|
||||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeRunId = null;
|
||||
_activeRunSeed = 0;
|
||||
_activeNodeId = 0;
|
||||
_activeNodeType = RunNodeType.None;
|
||||
_activeSequenceIndex = -1;
|
||||
_activeContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
|
|
@ -24,7 +22,7 @@ namespace GeometryTD.CustomEvent
|
|||
|
||||
public bool CombatWon { get; private set; }
|
||||
|
||||
public BackpackInventoryData InventorySnapshotAfterNode { get; private set; }
|
||||
public RunNodeCompletionSnapshot CompletionSnapshot { get; private set; }
|
||||
|
||||
public NodeCompleteEventArgs()
|
||||
{
|
||||
|
|
@ -42,7 +40,7 @@ namespace GeometryTD.CustomEvent
|
|||
int sequenceIndex,
|
||||
RunNodeCompletionStatus completionStatus,
|
||||
bool combatWon,
|
||||
BackpackInventoryData inventorySnapshotAfterNode)
|
||||
RunNodeCompletionSnapshot completionSnapshot)
|
||||
{
|
||||
var args = ReferencePool.Acquire<NodeCompleteEventArgs>();
|
||||
args.RunId = runId;
|
||||
|
|
@ -51,7 +49,7 @@ namespace GeometryTD.CustomEvent
|
|||
args.SequenceIndex = sequenceIndex;
|
||||
args.CompletionStatus = completionStatus;
|
||||
args.CombatWon = combatWon;
|
||||
args.InventorySnapshotAfterNode = InventoryCloneUtility.CloneInventory(inventorySnapshotAfterNode);
|
||||
args.CompletionSnapshot = completionSnapshot != null ? completionSnapshot.Clone() : null;
|
||||
|
||||
return args;
|
||||
}
|
||||
|
|
@ -64,7 +62,7 @@ namespace GeometryTD.CustomEvent
|
|||
SequenceIndex = -1;
|
||||
CompletionStatus = RunNodeCompletionStatus.None;
|
||||
CombatWon = false;
|
||||
InventorySnapshotAfterNode = null;
|
||||
CompletionSnapshot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,23 +194,36 @@ namespace GeometryTD.Procedure
|
|||
args.CompletionStatus,
|
||||
args.CombatWon);
|
||||
|
||||
BackpackInventoryData snapshot = args.InventorySnapshotAfterNode;
|
||||
if (snapshot == null && GameEntry.PlayerInventory != null)
|
||||
RunNodeCompletionSnapshot completionSnapshot = args.CompletionSnapshot != null
|
||||
? args.CompletionSnapshot.Clone()
|
||||
: _currentRunState?.CreateCurrentNodeContext()?.CreateCompletionSnapshot(null);
|
||||
BackpackInventoryData inventorySnapshot = completionSnapshot?.InventorySnapshot;
|
||||
if (inventorySnapshot == null && GameEntry.PlayerInventory != null)
|
||||
{
|
||||
snapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
|
||||
inventorySnapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
|
||||
}
|
||||
|
||||
if (completionSnapshot != null)
|
||||
{
|
||||
completionSnapshot.InventorySnapshot = inventorySnapshot;
|
||||
}
|
||||
|
||||
ProcedureMainParticipantTowerCleanupResult cleanupResult =
|
||||
ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers(
|
||||
snapshot,
|
||||
inventorySnapshot,
|
||||
MaxParticipantTowerCount);
|
||||
if (cleanupResult.HasAnyRemovedTower && GameEntry.PlayerInventory != null)
|
||||
{
|
||||
GameEntry.PlayerInventory.ReplaceInventorySnapshot(snapshot);
|
||||
GameEntry.PlayerInventory.ReplaceInventorySnapshot(inventorySnapshot);
|
||||
}
|
||||
|
||||
if (completionSnapshot != null)
|
||||
{
|
||||
completionSnapshot.InventorySnapshot = inventorySnapshot;
|
||||
}
|
||||
|
||||
ProcedureMainRunAdvanceResult advanceResult =
|
||||
ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, snapshot);
|
||||
ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, completionSnapshot);
|
||||
HandleRunAdvanceResult(advanceResult);
|
||||
if (cleanupResult.HasAnyRemovedTower &&
|
||||
advanceResult != ProcedureMainRunAdvanceResult.RunCompleted)
|
||||
|
|
@ -266,28 +279,20 @@ namespace GeometryTD.Procedure
|
|||
return;
|
||||
}
|
||||
|
||||
if (GameEntry.CombatNode.CurrentThemeType != currentNode.ThemeType)
|
||||
{
|
||||
GameEntry.CombatNode.OnInit(currentNode.ThemeType);
|
||||
}
|
||||
|
||||
GameEntry.CombatNode.StartCombat(
|
||||
currentNode.LinkedLevelId,
|
||||
_currentRunState.RunId,
|
||||
_currentRunState.RunSeed,
|
||||
currentNode.NodeId,
|
||||
currentNode.NodeType,
|
||||
currentNode.SequenceIndex);
|
||||
_currentRunState.CreateCurrentNodeContext());
|
||||
return;
|
||||
case RunNodeType.Event:
|
||||
GameEntry.EventNode.StartEvent(
|
||||
_currentRunState.RunId,
|
||||
currentNode.NodeId,
|
||||
currentNode.NodeType,
|
||||
currentNode.SequenceIndex);
|
||||
GameEntry.EventNode.StartEvent(_currentRunState.CreateCurrentNodeContext());
|
||||
return;
|
||||
case RunNodeType.Shop:
|
||||
GameEntry.ShopNode.StartShop(
|
||||
_currentRunState.RunId,
|
||||
_currentRunState.RunSeed,
|
||||
currentNode.NodeId,
|
||||
currentNode.NodeType,
|
||||
currentNode.SequenceIndex);
|
||||
GameEntry.ShopNode.StartShop(_currentRunState.CreateCurrentNodeContext());
|
||||
return;
|
||||
default:
|
||||
Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.",
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ namespace GeometryTD.Procedure
|
|||
public static ProcedureMainRunAdvanceResult TryAdvanceRun(
|
||||
RunState runState,
|
||||
RunNodeCompletionStatus completionStatus,
|
||||
BackpackInventoryData snapshot)
|
||||
RunNodeCompletionSnapshot completionSnapshot)
|
||||
{
|
||||
if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, snapshot))
|
||||
if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, completionSnapshot))
|
||||
{
|
||||
return ProcedureMainRunAdvanceResult.NoChange;
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@ namespace GeometryTD.Procedure
|
|||
: ProcedureMainRunAdvanceResult.AdvancedToNextNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,96 @@ namespace GeometryTD.Procedure
|
|||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunItemState
|
||||
{
|
||||
public int ItemId { get; set; }
|
||||
public int StackCount { get; set; }
|
||||
|
||||
internal RunItemState Clone()
|
||||
{
|
||||
return new RunItemState
|
||||
{
|
||||
ItemId = ItemId,
|
||||
StackCount = StackCount
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunNodeExecutionContext
|
||||
{
|
||||
public string RunId { get; set; }
|
||||
public int RunSeed { get; set; }
|
||||
public int NodeId { get; set; }
|
||||
public RunNodeType NodeType { get; set; }
|
||||
public int SequenceIndex { get; set; }
|
||||
public LevelThemeType ThemeType { get; set; }
|
||||
public int ThemeStageIndex { get; set; }
|
||||
public List<LevelThemeType> CurrentThemePool { get; set; } = new List<LevelThemeType>();
|
||||
public List<LevelThemeType> ThemeHistory { get; set; } = new List<LevelThemeType>();
|
||||
public int CurrentNodeContinueChallengeLayer { get; set; }
|
||||
public List<RunItemState> RunItems { get; set; } = new List<RunItemState>();
|
||||
|
||||
internal RunNodeExecutionContext Clone()
|
||||
{
|
||||
return new RunNodeExecutionContext
|
||||
{
|
||||
RunId = RunId,
|
||||
RunSeed = RunSeed,
|
||||
NodeId = NodeId,
|
||||
NodeType = NodeType,
|
||||
SequenceIndex = SequenceIndex,
|
||||
ThemeType = ThemeType,
|
||||
ThemeStageIndex = ThemeStageIndex,
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
|
||||
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
|
||||
};
|
||||
}
|
||||
|
||||
public RunNodeCompletionSnapshot CreateCompletionSnapshot(BackpackInventoryData inventorySnapshot)
|
||||
{
|
||||
return new RunNodeCompletionSnapshot
|
||||
{
|
||||
InventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot),
|
||||
ThemeType = ThemeType,
|
||||
ThemeStageIndex = ThemeStageIndex,
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
|
||||
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunNodeCompletionSnapshot
|
||||
{
|
||||
public BackpackInventoryData InventorySnapshot { get; set; }
|
||||
public LevelThemeType ThemeType { get; set; }
|
||||
public int ThemeStageIndex { get; set; }
|
||||
public List<LevelThemeType> CurrentThemePool { get; set; } = new List<LevelThemeType>();
|
||||
public List<LevelThemeType> ThemeHistory { get; set; } = new List<LevelThemeType>();
|
||||
public int CurrentNodeContinueChallengeLayer { get; set; }
|
||||
public List<RunItemState> RunItems { get; set; } = new List<RunItemState>();
|
||||
|
||||
internal RunNodeCompletionSnapshot Clone()
|
||||
{
|
||||
return new RunNodeCompletionSnapshot
|
||||
{
|
||||
InventorySnapshot = InventoryCloneUtility.CloneInventory(InventorySnapshot),
|
||||
ThemeType = ThemeType,
|
||||
ThemeStageIndex = ThemeStageIndex,
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
|
||||
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunState
|
||||
{
|
||||
|
|
@ -82,6 +172,11 @@ namespace GeometryTD.Procedure
|
|||
ThemeType = themeType;
|
||||
_nodes = nodes ?? new List<RunNodeState>();
|
||||
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(runInventorySnapshot);
|
||||
ThemeStageIndex = 0;
|
||||
CurrentThemePool = CreateDefaultThemeList(themeType);
|
||||
ThemeHistory = CreateDefaultThemeList(themeType);
|
||||
CurrentNodeContinueChallengeLayer = 0;
|
||||
RunItems = new List<RunItemState>();
|
||||
CurrentNodeIndex = _nodes.Count > 0 ? 0 : -1;
|
||||
IsCompleted = _nodes.Count <= 0;
|
||||
}
|
||||
|
|
@ -98,6 +193,16 @@ namespace GeometryTD.Procedure
|
|||
|
||||
public BackpackInventoryData RunInventorySnapshot { get; internal set; }
|
||||
|
||||
public int ThemeStageIndex { get; internal set; }
|
||||
|
||||
public List<LevelThemeType> CurrentThemePool { get; internal set; }
|
||||
|
||||
public List<LevelThemeType> ThemeHistory { get; internal set; }
|
||||
|
||||
public int CurrentNodeContinueChallengeLayer { get; internal set; }
|
||||
|
||||
public List<RunItemState> RunItems { get; internal set; }
|
||||
|
||||
public IReadOnlyList<RunNodeState> Nodes => _nodes;
|
||||
|
||||
public int NodeCount => _nodes.Count;
|
||||
|
|
@ -161,5 +266,90 @@ namespace GeometryTD.Procedure
|
|||
{
|
||||
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot);
|
||||
}
|
||||
|
||||
public RunNodeExecutionContext CreateCurrentNodeContext()
|
||||
{
|
||||
RunNodeState currentNode = CurrentNode;
|
||||
if (currentNode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RunNodeExecutionContext
|
||||
{
|
||||
RunId = RunId,
|
||||
RunSeed = RunSeed,
|
||||
NodeId = currentNode.NodeId,
|
||||
NodeType = currentNode.NodeType,
|
||||
SequenceIndex = currentNode.SequenceIndex,
|
||||
ThemeType = ThemeType,
|
||||
ThemeStageIndex = ThemeStageIndex,
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
|
||||
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyCompletionSnapshot(RunNodeCompletionSnapshot completionSnapshot)
|
||||
{
|
||||
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(completionSnapshot.InventorySnapshot);
|
||||
ThemeType = completionSnapshot.ThemeType;
|
||||
ThemeStageIndex = completionSnapshot.ThemeStageIndex;
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(completionSnapshot.CurrentThemePool);
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(completionSnapshot.ThemeHistory);
|
||||
CurrentNodeContinueChallengeLayer = completionSnapshot.CurrentNodeContinueChallengeLayer;
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(completionSnapshot.RunItems);
|
||||
}
|
||||
|
||||
private static List<LevelThemeType> CreateDefaultThemeList(LevelThemeType themeType)
|
||||
{
|
||||
List<LevelThemeType> themes = new List<LevelThemeType>();
|
||||
if (themeType != LevelThemeType.None)
|
||||
{
|
||||
themes.Add(themeType);
|
||||
}
|
||||
|
||||
return themes;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class RunStateCloneUtility
|
||||
{
|
||||
internal static List<LevelThemeType> CloneThemeList(IReadOnlyList<LevelThemeType> source)
|
||||
{
|
||||
List<LevelThemeType> cloned = new List<LevelThemeType>();
|
||||
if (source == null)
|
||||
{
|
||||
return cloned;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
cloned.Add(source[i]);
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
internal static List<RunItemState> CloneRunItems(IReadOnlyList<RunItemState> source)
|
||||
{
|
||||
List<RunItemState> cloned = new List<RunItemState>();
|
||||
if (source == null)
|
||||
{
|
||||
return cloned;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
RunItemState item = source[i];
|
||||
if (item != null)
|
||||
{
|
||||
cloned.Add(item.Clone());
|
||||
}
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.Procedure
|
||||
{
|
||||
public static class RunStateAdvanceService
|
||||
|
|
@ -7,7 +5,7 @@ namespace GeometryTD.Procedure
|
|||
public static bool TryCompleteCurrentNode(
|
||||
RunState runState,
|
||||
RunNodeCompletionStatus completionStatus,
|
||||
BackpackInventoryData inventorySnapshotAfterNode)
|
||||
RunNodeCompletionSnapshot completionSnapshot)
|
||||
{
|
||||
if (runState == null || runState.IsCompleted)
|
||||
{
|
||||
|
|
@ -20,7 +18,7 @@ namespace GeometryTD.Procedure
|
|||
return false;
|
||||
}
|
||||
|
||||
runState.ReplaceInventorySnapshot(inventorySnapshotAfterNode);
|
||||
runState.ApplyCompletionSnapshot(completionSnapshot);
|
||||
|
||||
if (completionStatus == RunNodeCompletionStatus.Exception)
|
||||
{
|
||||
|
|
@ -38,12 +36,14 @@ namespace GeometryTD.Procedure
|
|||
int nextIndex = runState.CurrentNodeIndex + 1;
|
||||
if (nextIndex >= runState.Nodes.Count)
|
||||
{
|
||||
runState.CurrentNodeContinueChallengeLayer = 0;
|
||||
runState.CurrentNodeIndex = runState.Nodes.Count;
|
||||
runState.IsCompleted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
runState.CurrentNodeIndex = nextIndex;
|
||||
runState.CurrentNodeContinueChallengeLayer = 0;
|
||||
RunNodeState nextNode = runState.CurrentNode;
|
||||
if (nextNode != null && nextNode.Status == RunNodeStatus.Locked)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GeometryTD.Tests.EditMode
|
||||
{
|
||||
public sealed class ContinueChallengeMultiplierTests
|
||||
{
|
||||
[Test]
|
||||
public void BuildRawData_Uses_OneX_Multiplier_For_First_Loop_And_Invalid_Input()
|
||||
{
|
||||
CombatInfoFormRawData firstLoop = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Plain,
|
||||
levelId: 1,
|
||||
currentPhaseIndex: 1,
|
||||
totalPhaseCount: 4,
|
||||
coin: 10,
|
||||
baseHp: 20,
|
||||
baseHpMax: 20,
|
||||
canEnd: true);
|
||||
CombatInfoFormRawData invalidInput = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Plain,
|
||||
levelId: 1,
|
||||
currentPhaseIndex: 0,
|
||||
totalPhaseCount: 0,
|
||||
coin: 10,
|
||||
baseHp: 20,
|
||||
baseHpMax: 20,
|
||||
canEnd: false);
|
||||
|
||||
Assert.That(firstLoop.EnemyHpRateMultiplier, Is.EqualTo(1));
|
||||
Assert.That(invalidInput.EnemyHpRateMultiplier, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildRawData_Doubles_EnemyHp_After_Each_Full_Loop()
|
||||
{
|
||||
CombatInfoFormRawData secondLoop = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Volcano,
|
||||
levelId: 2,
|
||||
currentPhaseIndex: 5,
|
||||
totalPhaseCount: 4,
|
||||
coin: 15,
|
||||
baseHp: 18,
|
||||
baseHpMax: 20,
|
||||
canEnd: true);
|
||||
CombatInfoFormRawData thirdLoop = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Mountain,
|
||||
levelId: 3,
|
||||
currentPhaseIndex: 9,
|
||||
totalPhaseCount: 4,
|
||||
coin: 20,
|
||||
baseHp: 16,
|
||||
baseHpMax: 20,
|
||||
canEnd: true);
|
||||
|
||||
Assert.That(secondLoop.EnemyHpRateMultiplier, Is.EqualTo(2));
|
||||
Assert.That(thirdLoop.EnemyHpRateMultiplier, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildRawData_Clamps_EnemyHp_Multiplier_At_IntMaxValue()
|
||||
{
|
||||
CombatInfoFormRawData rawData = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Plain,
|
||||
levelId: 1,
|
||||
currentPhaseIndex: 121,
|
||||
totalPhaseCount: 4,
|
||||
coin: 30,
|
||||
baseHp: 10,
|
||||
baseHpMax: 20,
|
||||
canEnd: false);
|
||||
|
||||
Assert.That(rawData.EnemyHpRateMultiplier, Is.EqualTo(int.MaxValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 98b9356f64b87bb4ba1d0bd65081b224
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GeometryTD.Tests.EditMode
|
||||
{
|
||||
public sealed class EventDefinitionFactoryTests
|
||||
{
|
||||
[Test]
|
||||
public void RequirementFactory_Creates_GoldAtLeast_From_Count_Or_Gold_Field()
|
||||
{
|
||||
EventRequirementBase countRequirement = EventRequirementFactory.Create(
|
||||
"GoldAtLeast",
|
||||
JObject.Parse(@"{ ""Count"": 35 }"));
|
||||
EventRequirementBase goldRequirement = EventRequirementFactory.Create(
|
||||
"GoldAtLeast",
|
||||
JObject.Parse(@"{ ""Gold"": 21 }"));
|
||||
|
||||
Assert.That(countRequirement, Is.TypeOf<GoldAtLeastRequirement>());
|
||||
Assert.That(((GoldAtLeastParam)countRequirement.Param).Gold, Is.EqualTo(35));
|
||||
Assert.That(goldRequirement, Is.TypeOf<GoldAtLeastRequirement>());
|
||||
Assert.That(((GoldAtLeastParam)goldRequirement.Param).Gold, Is.EqualTo(21));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RequirementFactory_Creates_Component_And_Relic_Requirements_With_Expected_Params()
|
||||
{
|
||||
EventRequirementBase compRequirement = EventRequirementFactory.Create(
|
||||
"CompCountAtLeast",
|
||||
JObject.Parse(@"{ ""Count"": 2, ""Rarity"": ""Blue"" }"));
|
||||
EventRequirementBase relicRequirement = EventRequirementFactory.Create(
|
||||
"HasRelic",
|
||||
JObject.Parse(@"{ ""Id"": 9001 }"));
|
||||
|
||||
Assert.That(compRequirement, Is.TypeOf<CompCountAtLeastRequirement>());
|
||||
CompCountAtLeastParam compParam = (CompCountAtLeastParam)compRequirement.Param;
|
||||
Assert.That(compParam.Count, Is.EqualTo(2));
|
||||
Assert.That(compParam.Rarity, Is.EqualTo(RarityType.Blue));
|
||||
|
||||
Assert.That(relicRequirement, Is.TypeOf<HasRelicRequirement>());
|
||||
Assert.That(((HasRelicParam)relicRequirement.Param).RelicId, Is.EqualTo(9001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EffectFactory_Creates_AddGold_And_Component_Effects_With_Probability()
|
||||
{
|
||||
EventEffectBase addGoldEffect = EventEffectFactory.Create(
|
||||
"AddGold",
|
||||
JObject.Parse(@"{ ""Count"": 80 }"),
|
||||
0.75f);
|
||||
EventEffectBase addRandomCompEffect = EventEffectFactory.Create(
|
||||
"AddRandomComps",
|
||||
JObject.Parse(@"{ ""Count"": 1, ""Rarity"": ""Purple"" }"),
|
||||
0.5f);
|
||||
EventEffectBase removeRandomCompEffect = EventEffectFactory.Create(
|
||||
"RemoveRandomComps",
|
||||
JObject.Parse(@"{ ""Count"": 2, ""Rarity"": ""Blue"" }"),
|
||||
0.25f);
|
||||
|
||||
Assert.That(addGoldEffect, Is.TypeOf<AddGoldEffect>());
|
||||
Assert.That(((AddGoldParam)addGoldEffect.Param).Count, Is.EqualTo(80));
|
||||
Assert.That(addGoldEffect.Probability, Is.EqualTo(0.75f));
|
||||
|
||||
Assert.That(addRandomCompEffect, Is.TypeOf<AddRandomCompsEffect>());
|
||||
AddRandomCompsParam addParam = (AddRandomCompsParam)addRandomCompEffect.Param;
|
||||
Assert.That(addParam.Count, Is.EqualTo(1));
|
||||
Assert.That(addParam.Rarity, Is.EqualTo(RarityType.Purple));
|
||||
Assert.That(addRandomCompEffect.Probability, Is.EqualTo(0.5f));
|
||||
|
||||
Assert.That(removeRandomCompEffect, Is.TypeOf<RemoveRandomCompsEffect>());
|
||||
RemoveRandomCompsParam removeParam = (RemoveRandomCompsParam)removeRandomCompEffect.Param;
|
||||
Assert.That(removeParam.Count, Is.EqualTo(2));
|
||||
Assert.That(removeParam.Rarity, Is.EqualTo(RarityType.Blue));
|
||||
Assert.That(removeRandomCompEffect.Probability, Is.EqualTo(0.25f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EffectFactory_Creates_DamageRandomTowerEnduranceEffect_With_Expected_Params()
|
||||
{
|
||||
EventEffectBase effect = EventEffectFactory.Create(
|
||||
"DamageRandomTowersEndurance",
|
||||
JObject.Parse(@"{ ""Count"": 3, ""Amount"": 15 }"),
|
||||
0.4f);
|
||||
|
||||
Assert.That(effect, Is.TypeOf<DamageRandomTowerEnduranceEffect>());
|
||||
DamageRandomTowerEnduranceParam param = (DamageRandomTowerEnduranceParam)effect.Param;
|
||||
Assert.That(param.Count, Is.EqualTo(3));
|
||||
Assert.That(param.Amount, Is.EqualTo(15));
|
||||
Assert.That(effect.Probability, Is.EqualTo(0.4f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Factories_Return_Null_For_Unsupported_Or_Empty_Type()
|
||||
{
|
||||
Assert.That(EventRequirementFactory.Create(string.Empty, JObject.Parse("{}")), Is.Null);
|
||||
Assert.That(EventRequirementFactory.Create("UnknownRequirement", JObject.Parse("{}")), Is.Null);
|
||||
Assert.That(EventEffectFactory.Create(string.Empty, JObject.Parse("{}")), Is.Null);
|
||||
Assert.That(EventEffectFactory.Create("UnknownEffect", JObject.Parse("{}")), Is.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 822dbd88944ebf24faf09b2acd38a1a2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -3,7 +3,9 @@
|
|||
"rootNamespace": "GeometryTD.Tests.EditMode",
|
||||
"references": [
|
||||
"GeometryTD.Runtime",
|
||||
"UnityGameFramework.Runtime"
|
||||
"UnityGameFramework.Runtime",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
|
|
@ -12,13 +14,14 @@
|
|||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"GameFramework.dll"
|
||||
"GameFramework.dll",
|
||||
"nunit.framework.dll",
|
||||
"Newtonsoft.Json.dll"
|
||||
],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [],
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false,
|
||||
"optionalUnityReferences": [
|
||||
"TestAssemblies"
|
||||
]
|
||||
}
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
0,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
new BackpackInventoryData { Gold = 77 }));
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 77 })));
|
||||
|
||||
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1));
|
||||
|
|
@ -133,7 +133,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
0,
|
||||
RunNodeCompletionStatus.Exception,
|
||||
false,
|
||||
new BackpackInventoryData { Gold = 12 }));
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 12 })));
|
||||
|
||||
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
|
||||
|
|
@ -165,7 +165,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
0,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
brokenSnapshot));
|
||||
CreateCompletionSnapshot(brokenSnapshot)));
|
||||
|
||||
BackpackInventoryData replacedInventory = inventoryComponent.GetInventorySnapshot();
|
||||
DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData;
|
||||
|
|
@ -180,7 +180,8 @@ namespace GeometryTD.Tests.EditMode
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void OnNodeComplete_Boss_Run_Completed_Shows_Run_Complete_Dialog_And_Does_Not_Override_It_With_Cleanup_Dialog()
|
||||
public void
|
||||
OnNodeComplete_Boss_Run_Completed_Shows_Run_Complete_Dialog_And_Does_Not_Override_It_With_Cleanup_Dialog()
|
||||
{
|
||||
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
|
||||
PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateParticipantInventory(100f));
|
||||
|
|
@ -207,7 +208,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
0,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
brokenSnapshot));
|
||||
CreateCompletionSnapshot(brokenSnapshot)));
|
||||
|
||||
BackpackInventoryData replacedInventory = inventoryComponent.GetInventorySnapshot();
|
||||
DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData;
|
||||
|
|
@ -220,7 +221,8 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1));
|
||||
Assert.That(dialogRawData, Is.Not.Null);
|
||||
Assert.That(dialogRawData.Title, Is.EqualTo("Run Complete"));
|
||||
Assert.That(dialogRawData.Message, Is.EqualTo("Boss node completed. This run is finished and will return to the main menu."));
|
||||
Assert.That(dialogRawData.Message,
|
||||
Is.EqualTo("Boss node completed. This run is finished and will return to the main menu."));
|
||||
Assert.That(dialogRawData.OnClickConfirm, Is.Not.Null);
|
||||
|
||||
dialogRawData.OnClickConfirm(null);
|
||||
|
|
@ -428,7 +430,8 @@ namespace GeometryTD.Tests.EditMode
|
|||
new BackpackInventoryData { Gold = 50 },
|
||||
new[]
|
||||
{
|
||||
new RunNodeSeed { NodeId = 101, NodeType = RunNodeType.Combat, LinkedLevelId = 1, SequenceIndex = 0 },
|
||||
new RunNodeSeed
|
||||
{ NodeId = 101, NodeType = RunNodeType.Combat, LinkedLevelId = 1, SequenceIndex = 0 },
|
||||
new RunNodeSeed { NodeId = 102, NodeType = RunNodeType.Event, SequenceIndex = 1 }
|
||||
},
|
||||
"testrun");
|
||||
|
|
@ -467,6 +470,20 @@ namespace GeometryTD.Tests.EditMode
|
|||
return inventory;
|
||||
}
|
||||
|
||||
private static RunNodeCompletionSnapshot CreateCompletionSnapshot(BackpackInventoryData inventorySnapshot)
|
||||
{
|
||||
return new RunNodeCompletionSnapshot
|
||||
{
|
||||
InventorySnapshot = inventorySnapshot,
|
||||
ThemeType = LevelThemeType.Plain,
|
||||
ThemeStageIndex = 0,
|
||||
CurrentThemePool = new List<LevelThemeType> { LevelThemeType.Plain },
|
||||
ThemeHistory = new List<LevelThemeType> { LevelThemeType.Plain },
|
||||
CurrentNodeContinueChallengeLayer = 0,
|
||||
RunItems = new List<RunItemState>()
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class RecorderUIRouter
|
||||
{
|
||||
private readonly Dictionary<UIFormType, RecorderController> _controllers = new();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
new BackpackInventoryData { Gold = 88 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 88 }, continueChallengeLayer: 4));
|
||||
|
||||
Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.AdvancedToNextNode));
|
||||
Assert.That(runState.IsCompleted, Is.False);
|
||||
|
|
@ -24,6 +24,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1));
|
||||
Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Available));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(88));
|
||||
Assert.That(runState.CurrentNodeContinueChallengeLayer, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -34,7 +35,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Exception,
|
||||
new BackpackInventoryData { Gold = 5 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 5 }, continueChallengeLayer: 2));
|
||||
|
||||
Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.NodeException));
|
||||
Assert.That(runState.IsCompleted, Is.False);
|
||||
|
|
@ -42,6 +43,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Exception));
|
||||
Assert.That(runState.Nodes[1].Status, Is.EqualTo(RunNodeStatus.Locked));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(5));
|
||||
Assert.That(runState.CurrentNodeContinueChallengeLayer, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -58,7 +60,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
new BackpackInventoryData { Gold = 123 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 123 }));
|
||||
|
||||
Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.RunCompleted));
|
||||
Assert.That(runState.IsCompleted, Is.True);
|
||||
|
|
@ -73,7 +75,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
ProcedureMainRunAdvanceResult nullRunResult = ProcedureMainRunFlowService.TryAdvanceRun(
|
||||
null,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
new BackpackInventoryData { Gold = 1 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 1 }));
|
||||
|
||||
Assert.That(nullRunResult, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange));
|
||||
|
||||
|
|
@ -85,7 +87,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
ProcedureMainRunAdvanceResult completedRunResult = ProcedureMainRunFlowService.TryAdvanceRun(
|
||||
completedRun,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
new BackpackInventoryData { Gold = 20 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 20 }));
|
||||
|
||||
Assert.That(completedRunResult, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange));
|
||||
Assert.That(completedRun.RunInventorySnapshot.Gold, Is.EqualTo(10));
|
||||
|
|
@ -99,12 +101,13 @@ namespace GeometryTD.Tests.EditMode
|
|||
ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun(
|
||||
runState,
|
||||
RunNodeCompletionStatus.None,
|
||||
new BackpackInventoryData { Gold = 999 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 999 }, themeStageIndex: 4));
|
||||
|
||||
Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange));
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
|
||||
Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Available));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(999));
|
||||
Assert.That(runState.ThemeStageIndex, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -462,5 +465,31 @@ namespace GeometryTD.Tests.EditMode
|
|||
inventory.ParticipantTowerInstanceIds.Add(90001);
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private static RunNodeCompletionSnapshot CreateCompletionSnapshot(
|
||||
BackpackInventoryData inventorySnapshot,
|
||||
LevelThemeType themeType = LevelThemeType.Plain,
|
||||
int themeStageIndex = 0,
|
||||
int continueChallengeLayer = 0)
|
||||
{
|
||||
RunNodeCompletionSnapshot snapshot = new RunNodeCompletionSnapshot
|
||||
{
|
||||
InventorySnapshot = inventorySnapshot,
|
||||
ThemeType = themeType,
|
||||
ThemeStageIndex = themeStageIndex,
|
||||
CurrentThemePool = new System.Collections.Generic.List<LevelThemeType>(),
|
||||
ThemeHistory = new System.Collections.Generic.List<LevelThemeType>(),
|
||||
CurrentNodeContinueChallengeLayer = continueChallengeLayer,
|
||||
RunItems = new System.Collections.Generic.List<RunItemState>()
|
||||
};
|
||||
|
||||
if (themeType != LevelThemeType.None)
|
||||
{
|
||||
snapshot.CurrentThemePool.Add(themeType);
|
||||
snapshot.ThemeHistory.Add(themeType);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Available));
|
||||
Assert.That(runState.Nodes[1].Status, Is.EqualTo(RunNodeStatus.Locked));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(120));
|
||||
Assert.That(runState.ThemeStageIndex, Is.EqualTo(0));
|
||||
Assert.That(runState.CurrentThemePool, Is.EqualTo(new[] { LevelThemeType.Plain }));
|
||||
Assert.That(runState.ThemeHistory, Is.EqualTo(new[] { LevelThemeType.Plain }));
|
||||
Assert.That(runState.CurrentNodeContinueChallengeLayer, Is.EqualTo(0));
|
||||
Assert.That(runState.RunItems, Is.Empty);
|
||||
|
||||
sourceInventory.Gold = 1;
|
||||
sourceInventory.ParticipantTowerInstanceIds[0] = 99;
|
||||
|
|
@ -62,22 +67,30 @@ namespace GeometryTD.Tests.EditMode
|
|||
bool firstCompleted = RunStateAdvanceService.TryCompleteCurrentNode(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
new BackpackInventoryData { Gold = 80 });
|
||||
CreateCompletionSnapshot(
|
||||
new BackpackInventoryData { Gold = 80 },
|
||||
continueChallengeLayer: 3,
|
||||
runItemId: 1001,
|
||||
runItemStackCount: 2));
|
||||
|
||||
Assert.That(firstCompleted, Is.True);
|
||||
Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Completed));
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1));
|
||||
Assert.That(runState.Nodes[1].Status, Is.EqualTo(RunNodeStatus.Available));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(80));
|
||||
Assert.That(runState.CurrentNodeContinueChallengeLayer, Is.EqualTo(0));
|
||||
Assert.That(runState.RunItems.Count, Is.EqualTo(1));
|
||||
Assert.That(runState.RunItems[0].ItemId, Is.EqualTo(1001));
|
||||
Assert.That(runState.RunItems[0].StackCount, Is.EqualTo(2));
|
||||
|
||||
RunStateAdvanceService.TryCompleteCurrentNode(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
new BackpackInventoryData { Gold = 95 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 95 }));
|
||||
RunStateAdvanceService.TryCompleteCurrentNode(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
new BackpackInventoryData { Gold = 130 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 130 }));
|
||||
|
||||
Assert.That(runState.IsCompleted, Is.True);
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(3));
|
||||
|
|
@ -124,19 +137,26 @@ namespace GeometryTD.Tests.EditMode
|
|||
bool result = RunStateAdvanceService.TryCompleteCurrentNode(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Exception,
|
||||
new BackpackInventoryData { Gold = 5 });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 5 }, continueChallengeLayer: 4));
|
||||
|
||||
Assert.That(result, Is.True);
|
||||
Assert.That(runState.IsCompleted, Is.False);
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
|
||||
Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Exception));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(5));
|
||||
Assert.That(runState.CurrentNodeContinueChallengeLayer, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NodeCompleteEventArgs_Completed_Win_Clones_Inventory_And_Clears_State()
|
||||
public void NodeCompleteEventArgs_Completed_Win_Clones_CompletionSnapshot_And_Clears_State()
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData { Gold = 66 };
|
||||
RunNodeCompletionSnapshot completionSnapshot = CreateCompletionSnapshot(
|
||||
new BackpackInventoryData { Gold = 66 },
|
||||
themeType: LevelThemeType.Volcano,
|
||||
themeStageIndex: 2,
|
||||
continueChallengeLayer: 5,
|
||||
runItemId: 901,
|
||||
runItemStackCount: 3);
|
||||
NodeCompleteEventArgs eventArgs = NodeCompleteEventArgs.Create(
|
||||
"run-1",
|
||||
7,
|
||||
|
|
@ -144,9 +164,11 @@ namespace GeometryTD.Tests.EditMode
|
|||
3,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
inventory);
|
||||
completionSnapshot);
|
||||
|
||||
inventory.Gold = 1;
|
||||
completionSnapshot.InventorySnapshot.Gold = 1;
|
||||
completionSnapshot.CurrentThemePool[0] = LevelThemeType.Mountain;
|
||||
completionSnapshot.RunItems[0].StackCount = 99;
|
||||
|
||||
Assert.That(eventArgs.RunId, Is.EqualTo("run-1"));
|
||||
Assert.That(eventArgs.NodeId, Is.EqualTo(7));
|
||||
|
|
@ -154,7 +176,11 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(eventArgs.SequenceIndex, Is.EqualTo(3));
|
||||
Assert.That(eventArgs.CompletionStatus, Is.EqualTo(RunNodeCompletionStatus.Completed));
|
||||
Assert.That(eventArgs.CombatWon, Is.True);
|
||||
Assert.That(eventArgs.InventorySnapshotAfterNode.Gold, Is.EqualTo(66));
|
||||
Assert.That(eventArgs.CompletionSnapshot.InventorySnapshot.Gold, Is.EqualTo(66));
|
||||
Assert.That(eventArgs.CompletionSnapshot.ThemeType, Is.EqualTo(LevelThemeType.Volcano));
|
||||
Assert.That(eventArgs.CompletionSnapshot.ThemeStageIndex, Is.EqualTo(2));
|
||||
Assert.That(eventArgs.CompletionSnapshot.CurrentThemePool, Is.EqualTo(new[] { LevelThemeType.Volcano }));
|
||||
Assert.That(eventArgs.CompletionSnapshot.RunItems[0].StackCount, Is.EqualTo(3));
|
||||
|
||||
eventArgs.Clear();
|
||||
|
||||
|
|
@ -164,13 +190,13 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(eventArgs.SequenceIndex, Is.EqualTo(-1));
|
||||
Assert.That(eventArgs.CompletionStatus, Is.EqualTo(RunNodeCompletionStatus.None));
|
||||
Assert.That(eventArgs.CombatWon, Is.False);
|
||||
Assert.That(eventArgs.InventorySnapshotAfterNode, Is.Null);
|
||||
Assert.That(eventArgs.CompletionSnapshot, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NodeCompleteEventArgs_Completed_Loss_Preserves_NonWin_Semantics()
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData { Gold = 12 };
|
||||
RunNodeCompletionSnapshot completionSnapshot = CreateCompletionSnapshot(new BackpackInventoryData { Gold = 12 });
|
||||
NodeCompleteEventArgs eventArgs = NodeCompleteEventArgs.Create(
|
||||
"run-2",
|
||||
8,
|
||||
|
|
@ -178,9 +204,9 @@ namespace GeometryTD.Tests.EditMode
|
|||
4,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
false,
|
||||
inventory);
|
||||
completionSnapshot);
|
||||
|
||||
inventory.Gold = 99;
|
||||
completionSnapshot.InventorySnapshot.Gold = 99;
|
||||
|
||||
Assert.That(eventArgs.RunId, Is.EqualTo("run-2"));
|
||||
Assert.That(eventArgs.NodeId, Is.EqualTo(8));
|
||||
|
|
@ -188,13 +214,13 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(eventArgs.SequenceIndex, Is.EqualTo(4));
|
||||
Assert.That(eventArgs.CompletionStatus, Is.EqualTo(RunNodeCompletionStatus.Completed));
|
||||
Assert.That(eventArgs.CombatWon, Is.False);
|
||||
Assert.That(eventArgs.InventorySnapshotAfterNode.Gold, Is.EqualTo(12));
|
||||
Assert.That(eventArgs.CompletionSnapshot.InventorySnapshot.Gold, Is.EqualTo(12));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NodeCompleteEventArgs_Exception_Preserves_Exception_Semantics()
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData { Gold = 7 };
|
||||
RunNodeCompletionSnapshot completionSnapshot = CreateCompletionSnapshot(new BackpackInventoryData { Gold = 7 });
|
||||
NodeCompleteEventArgs eventArgs = NodeCompleteEventArgs.Create(
|
||||
"run-3",
|
||||
9,
|
||||
|
|
@ -202,9 +228,9 @@ namespace GeometryTD.Tests.EditMode
|
|||
5,
|
||||
RunNodeCompletionStatus.Exception,
|
||||
false,
|
||||
inventory);
|
||||
completionSnapshot);
|
||||
|
||||
inventory.Gold = 100;
|
||||
completionSnapshot.InventorySnapshot.Gold = 100;
|
||||
|
||||
Assert.That(eventArgs.RunId, Is.EqualTo("run-3"));
|
||||
Assert.That(eventArgs.NodeId, Is.EqualTo(9));
|
||||
|
|
@ -212,7 +238,54 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(eventArgs.SequenceIndex, Is.EqualTo(5));
|
||||
Assert.That(eventArgs.CompletionStatus, Is.EqualTo(RunNodeCompletionStatus.Exception));
|
||||
Assert.That(eventArgs.CombatWon, Is.False);
|
||||
Assert.That(eventArgs.InventorySnapshotAfterNode.Gold, Is.EqualTo(7));
|
||||
Assert.That(eventArgs.CompletionSnapshot.InventorySnapshot.Gold, Is.EqualTo(7));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateCurrentNodeContext_Clones_Run_State_Collections()
|
||||
{
|
||||
RunState runState = RunStateFactory.Create(
|
||||
LevelThemeType.Plain,
|
||||
new BackpackInventoryData { Gold = 20 },
|
||||
new[]
|
||||
{
|
||||
new RunNodeSeed { NodeId = 99, NodeType = RunNodeType.Combat, LinkedLevelId = 1 },
|
||||
new RunNodeSeed { NodeId = 100, NodeType = RunNodeType.Event }
|
||||
});
|
||||
|
||||
RunNodeCompletionSnapshot seededSnapshot = new RunNodeCompletionSnapshot
|
||||
{
|
||||
InventorySnapshot = new BackpackInventoryData { Gold = 35 },
|
||||
ThemeType = LevelThemeType.Mountain,
|
||||
ThemeStageIndex = 3,
|
||||
CurrentThemePool = new List<LevelThemeType> { LevelThemeType.Mountain, LevelThemeType.Volcano },
|
||||
ThemeHistory = new List<LevelThemeType> { LevelThemeType.Plain, LevelThemeType.Mountain },
|
||||
CurrentNodeContinueChallengeLayer = 6,
|
||||
RunItems = new List<RunItemState>
|
||||
{
|
||||
new RunItemState { ItemId = 2001, StackCount = 4 }
|
||||
}
|
||||
};
|
||||
RunStateAdvanceService.TryCompleteCurrentNode(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
seededSnapshot);
|
||||
runState.ApplyCompletionSnapshot(seededSnapshot);
|
||||
|
||||
RunNodeExecutionContext context = runState.CreateCurrentNodeContext();
|
||||
|
||||
runState.CurrentThemePool[0] = LevelThemeType.Plain;
|
||||
runState.ThemeHistory[0] = LevelThemeType.Volcano;
|
||||
runState.RunItems[0].StackCount = 1;
|
||||
|
||||
Assert.That(context.RunId, Is.EqualTo(runState.RunId));
|
||||
Assert.That(context.NodeId, Is.EqualTo(100));
|
||||
Assert.That(context.ThemeStageIndex, Is.EqualTo(3));
|
||||
Assert.That(context.CurrentThemePool, Is.EqualTo(new[] { LevelThemeType.Mountain, LevelThemeType.Volcano }));
|
||||
Assert.That(context.ThemeHistory, Is.EqualTo(new[] { LevelThemeType.Plain, LevelThemeType.Mountain }));
|
||||
Assert.That(context.CurrentNodeContinueChallengeLayer, Is.EqualTo(6));
|
||||
Assert.That(context.RunItems[0].ItemId, Is.EqualTo(2001));
|
||||
Assert.That(context.RunItems[0].StackCount, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -275,7 +348,7 @@ namespace GeometryTD.Tests.EditMode
|
|||
bool advanced = RunStateAdvanceService.TryCompleteCurrentNode(
|
||||
runState,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
new BackpackInventoryData { Gold = 100 + i });
|
||||
CreateCompletionSnapshot(new BackpackInventoryData { Gold = 100 + i }));
|
||||
|
||||
Assert.That(advanced, Is.True);
|
||||
}
|
||||
|
|
@ -285,5 +358,42 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(10));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(109));
|
||||
}
|
||||
|
||||
private static RunNodeCompletionSnapshot CreateCompletionSnapshot(
|
||||
BackpackInventoryData inventorySnapshot,
|
||||
LevelThemeType themeType = LevelThemeType.Plain,
|
||||
int themeStageIndex = 0,
|
||||
int continueChallengeLayer = 0,
|
||||
int runItemId = 0,
|
||||
int runItemStackCount = 0)
|
||||
{
|
||||
RunNodeCompletionSnapshot snapshot = new RunNodeCompletionSnapshot
|
||||
{
|
||||
InventorySnapshot = inventorySnapshot,
|
||||
ThemeType = themeType,
|
||||
ThemeStageIndex = themeStageIndex,
|
||||
CurrentThemePool = new List<LevelThemeType>(),
|
||||
ThemeHistory = new List<LevelThemeType>(),
|
||||
CurrentNodeContinueChallengeLayer = continueChallengeLayer,
|
||||
RunItems = new List<RunItemState>()
|
||||
};
|
||||
|
||||
if (themeType != LevelThemeType.None)
|
||||
{
|
||||
snapshot.CurrentThemePool.Add(themeType);
|
||||
snapshot.ThemeHistory.Add(themeType);
|
||||
}
|
||||
|
||||
if (runItemId > 0)
|
||||
{
|
||||
snapshot.RunItems.Add(new RunItemState
|
||||
{
|
||||
ItemId = runItemId,
|
||||
StackCount = runItemStackCount
|
||||
});
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,295 @@
|
|||
using System.Collections;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GeometryTD.Tests.EditMode
|
||||
{
|
||||
public sealed class ShopPricingRuleTests
|
||||
{
|
||||
[Test]
|
||||
public void BuildGoods_Uses_Price_Row_Range_Matching_Item_Rarity()
|
||||
{
|
||||
ShopGoodsBuilder builder = CreateBuilder(
|
||||
CreateShopPriceRow(1, RarityType.Blue, 30, 35),
|
||||
CreateShopPriceRow(2, RarityType.Purple, 60, 70));
|
||||
|
||||
List<GoodsItemRawData> goods = builder.BuildGoods(goodsCount: 12, runSeed: 3001, sequenceIndex: 4);
|
||||
|
||||
Assert.That(goods, Has.Count.EqualTo(12));
|
||||
for (int i = 0; i < goods.Count; i++)
|
||||
{
|
||||
GoodsItemRawData item = goods[i];
|
||||
Assert.That(item, Is.Not.Null);
|
||||
Assert.That(item.SourceItem, Is.Not.Null);
|
||||
|
||||
switch (item.SourceItem.Rarity)
|
||||
{
|
||||
case RarityType.Blue:
|
||||
Assert.That(item.Price, Is.InRange(30, 35));
|
||||
break;
|
||||
case RarityType.Purple:
|
||||
Assert.That(item.Price, Is.InRange(60, 70));
|
||||
break;
|
||||
default:
|
||||
Assert.Fail($"Unexpected rarity generated for goods item {i}: {item.SourceItem.Rarity}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildGoods_Is_Reproducible_For_Same_RunSeed_And_SequenceIndex()
|
||||
{
|
||||
ShopGoodsBuilder builder = CreateBuilder(
|
||||
CreateShopPriceRow(1, RarityType.Blue, 30, 35),
|
||||
CreateShopPriceRow(2, RarityType.Purple, 60, 70));
|
||||
|
||||
string first = BuildGoodsSignature(builder.BuildGoods(goodsCount: 4, runSeed: 901, sequenceIndex: 7));
|
||||
string second = BuildGoodsSignature(builder.BuildGoods(goodsCount: 4, runSeed: 901, sequenceIndex: 7));
|
||||
|
||||
Assert.That(second, Is.EqualTo(first));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResolveRandomPrice_Returns_Zero_When_Rarity_Has_No_Price_Row()
|
||||
{
|
||||
ShopGoodsBuilder builder = CreateBuilder(CreateShopPriceRow(1, RarityType.Blue, 30, 35));
|
||||
MethodInfo method = typeof(ShopGoodsBuilder).GetMethod(
|
||||
"ResolveRandomPrice",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(method, Is.Not.Null);
|
||||
|
||||
int resolvedPrice = (int)method.Invoke(builder, new object[] { RarityType.Red, new Random(7) });
|
||||
|
||||
Assert.That(resolvedPrice, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
private static ShopGoodsBuilder CreateBuilder(params DRShopPrice[] shopPriceRows)
|
||||
{
|
||||
return new ShopGoodsBuilder(
|
||||
shopPriceRows,
|
||||
new FakeDataTable<DRMuzzleComp>(CreateMuzzleRow(1, "火焰枪口")),
|
||||
new FakeDataTable<DRBearingComp>(CreateBearingRow(1, "寒冰轴承")),
|
||||
new FakeDataTable<DRBaseComp>(CreateBaseRow(1, "迅捷底座")));
|
||||
}
|
||||
|
||||
private static string BuildGoodsSignature(IReadOnlyList<GoodsItemRawData> goods)
|
||||
{
|
||||
List<string> parts = new List<string>(goods.Count);
|
||||
for (int i = 0; i < goods.Count; i++)
|
||||
{
|
||||
GoodsItemRawData item = goods[i];
|
||||
parts.Add($"{item.GoodsIndex}:{item.Price}:{item.SourceItem.SlotType}:{item.SourceItem.Rarity}:{item.SourceItem.ConfigId}");
|
||||
}
|
||||
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
|
||||
private static DRShopPrice CreateShopPriceRow(int id, RarityType rarity, int minPrice, int maxPrice)
|
||||
{
|
||||
DRShopPrice row = new DRShopPrice();
|
||||
Assert.That(row.ParseDataRow($"\t{id}\t\t{rarity}\t{minPrice}\t{maxPrice}", null), Is.True);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static DRMuzzleComp CreateMuzzleRow(int id, string name)
|
||||
{
|
||||
DRMuzzleComp row = new DRMuzzleComp();
|
||||
Assert.That(
|
||||
row.ParseDataRow($"\t{id}\t\t{name}\t[10,20,30,40,50]\t3\t0.15\tNormalBullet\t\t[Fire,Crit]", null),
|
||||
Is.True);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static DRBearingComp CreateBearingRow(int id, string name)
|
||||
{
|
||||
DRBearingComp row = new DRBearingComp();
|
||||
Assert.That(
|
||||
row.ParseDataRow($"\t{id}\t\t{name}\t[1,2,3,4,5]\t0.5\t[10,20,30,40,50]\t1\t\t[Ice,Shatter]", null),
|
||||
Is.True);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static DRBaseComp CreateBaseRow(int id, string name)
|
||||
{
|
||||
DRBaseComp row = new DRBaseComp();
|
||||
Assert.That(
|
||||
row.ParseDataRow($"\t{id}\t\t{name}\t[2,4,6,8,10]\t-0.25\tFire\t\t[Fire,Crit]", null),
|
||||
Is.True);
|
||||
return row;
|
||||
}
|
||||
|
||||
private sealed class FakeDataTable<TRow> : IDataTable<TRow> where TRow : class, IDataRow
|
||||
{
|
||||
private readonly Dictionary<int, TRow> _rowsById = new();
|
||||
|
||||
public FakeDataTable(params TRow[] rows)
|
||||
{
|
||||
if (rows == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rows.Length; i++)
|
||||
{
|
||||
TRow row = rows[i];
|
||||
if (row != null)
|
||||
{
|
||||
_rowsById[row.Id] = row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Name => typeof(TRow).Name;
|
||||
public string FullName => typeof(TRow).FullName;
|
||||
public Type Type => typeof(TRow);
|
||||
public int Count => _rowsById.Count;
|
||||
public TRow this[int id] => GetDataRow(id);
|
||||
public TRow MinIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[0]);
|
||||
public TRow MaxIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[^1]);
|
||||
|
||||
public bool HasDataRow(int id) => _rowsById.ContainsKey(id);
|
||||
public bool HasDataRow(Predicate<TRow> condition) => GetDataRow(condition) != null;
|
||||
public TRow GetDataRow(int id) => _rowsById.TryGetValue(id, out TRow row) ? row : null;
|
||||
|
||||
public TRow GetDataRow(Predicate<TRow> condition)
|
||||
{
|
||||
if (condition == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (TRow row in _rowsById.Values)
|
||||
{
|
||||
if (row != null && condition(row))
|
||||
{
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public TRow[] GetDataRows(Predicate<TRow> condition)
|
||||
{
|
||||
List<TRow> results = new();
|
||||
GetDataRows(condition, results);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public void GetDataRows(Predicate<TRow> condition, List<TRow> results)
|
||||
{
|
||||
results?.Clear();
|
||||
if (condition == null || results == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (TRow row in _rowsById.Values)
|
||||
{
|
||||
if (row != null && condition(row))
|
||||
{
|
||||
results.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TRow[] GetDataRows(Comparison<TRow> comparison)
|
||||
{
|
||||
List<TRow> results = new();
|
||||
GetDataRows(comparison, results);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public void GetDataRows(Comparison<TRow> comparison, List<TRow> results)
|
||||
{
|
||||
results?.Clear();
|
||||
if (results == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
results.AddRange(_rowsById.Values);
|
||||
if (comparison != null)
|
||||
{
|
||||
results.Sort(comparison);
|
||||
}
|
||||
}
|
||||
|
||||
public TRow[] GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison)
|
||||
{
|
||||
List<TRow> results = new();
|
||||
GetDataRows(condition, comparison, results);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public void GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison, List<TRow> results)
|
||||
{
|
||||
GetDataRows(condition, results);
|
||||
if (results != null && comparison != null)
|
||||
{
|
||||
results.Sort(comparison);
|
||||
}
|
||||
}
|
||||
|
||||
public TRow[] GetAllDataRows()
|
||||
{
|
||||
List<TRow> results = new();
|
||||
GetAllDataRows(results);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public void GetAllDataRows(List<TRow> results)
|
||||
{
|
||||
results?.Clear();
|
||||
if (results == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (int id in GetOrderedIds())
|
||||
{
|
||||
results.Add(_rowsById[id]);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddDataRow(string dataRowString, object userData) => throw new NotSupportedException();
|
||||
public bool AddDataRow(byte[] dataRowBytes, int startIndex, int length, object userData) => throw new NotSupportedException();
|
||||
public bool RemoveDataRow(int id) => _rowsById.Remove(id);
|
||||
|
||||
public void RemoveAllDataRows()
|
||||
{
|
||||
_rowsById.Clear();
|
||||
}
|
||||
|
||||
public IEnumerator<TRow> GetEnumerator()
|
||||
{
|
||||
foreach (int id in GetOrderedIds())
|
||||
{
|
||||
yield return _rowsById[id];
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
private int[] GetOrderedIds()
|
||||
{
|
||||
int[] ids = new int[_rowsById.Count];
|
||||
_rowsById.Keys.CopyTo(ids, 0);
|
||||
Array.Sort(ids);
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d8e3a4a971efff9458007b36792c9fb1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,240 +1,251 @@
|
|||
# CodeX TODO
|
||||
# CodeX M2 执行计划
|
||||
|
||||
最后更新:2026-03-13
|
||||
> 依据:`docs/TODO.md` 当前口径,默认 `M1` 已完成并已收口。
|
||||
> 目标:把 `M2(P1)- 核心深度` 拆成可直接开工的执行顺序、具体工作包与验收边界。
|
||||
|
||||
> 目标:把当前“随机组件产出 / 掉落 / 商店 / 3 选 1 奖励候选 / Tag 初始化入口”从分散实现收口成清晰模块。
|
||||
> 原则:先收运行时入口,再收重复领域逻辑,最后收 UI 编排;不把纯规则层误做成 `GameFrameworkComponent`。
|
||||
## 当前代码现状判断
|
||||
|
||||
## 这次重构要解决的问题
|
||||
- `EventNodeComponent`、`DREvent`、`EventForm` 已有最小骨架,`Event.txt` 已有 3 个样例事件,但 `EventFormUseCase.SelectOption()` 仍停留在 `TODO`,尚未真正执行条件校验、概率结算、消耗/奖励效果。
|
||||
- `ShopNodeComponent` 与 `ShopFormUseCase` 已支持“生成 4 个商品并购买组件”的最小链路,但还没有出售、复杂定价、耐久折价、卖塔加成,也没有把商店能力沉到统一规则入口。
|
||||
- `RunState` 已完成 A1 扩展,当前已承载 `runId / runSeed / current theme / theme stage / current theme pool / theme history / current node continue challenge layer / run items / node sequence / inventory snapshot`,并且事件、商店、战斗都已经改为通过统一节点完成快照回写这些状态。
|
||||
- `LevelThemeType` 已有 `Volcano / Mountain`,战斗侧已有按主题取关卡、掉落按主题偏置的基础入口,因此主题玩法更适合在现有战斗调度器上继续补钩子,而不是重做主流程。
|
||||
|
||||
- 商店、敌人掉落、满血 3 选 1 候选都在各自生成组件实例,逻辑分散,后续改规则要改多处。
|
||||
- `ShopFormUseCase`、旧 `EnemyDropResolver`、`CombatSettlementService` 都曾同时承担“流程 + 规则 + 数据组装”的混合职责。
|
||||
- 重构前 `runSeed` 只稳定了 Tag 生成,没有稳定整个组件产出流程;当前已补齐到商店、掉落、奖励候选与奖励展示顺序。
|
||||
- Tag 模块本身已经分成 `Generation / Aggregation / Combat / Metadata / Presentation`,但初始化入口还散在流程代码里。
|
||||
## 当前进度更新
|
||||
|
||||
## 重构边界
|
||||
- `A1` 已完成:`RunState` 长期状态字段、`RunNodeExecutionContext`、`RunNodeCompletionSnapshot`、`ProcedureMain` 统一推进写回链路都已落地。
|
||||
- `A2` 已完成:已经补上三组最小 `EditMode` 测试入口,分别覆盖事件定义工厂、商店价格规则入口、当前可见的继续挑战倍率公式。
|
||||
- 当前下一步应直接进入 `P1-01`,收口事件执行链路,不再继续停留在基础设施阶段。
|
||||
|
||||
### 适合抽成 `GameFrameworkComponent` 的模块
|
||||
## M2 总体推进顺序
|
||||
|
||||
1. `InventoryGenerationComponent`
|
||||
- 统一作为运行时“组件产出入口”。
|
||||
- 对外提供:
|
||||
- `BuildShopGoods(...)`
|
||||
- `ResolveEnemyDrop(...)`
|
||||
- `BuildRewardCandidates(...)`
|
||||
- 统一持有并传递 `runSeed`、`sequenceIndex`、随机上下文。
|
||||
1. 先收口事件系统 `P1-01 ~ P1-02`,因为代码骨架已经在,投入最小,能最快形成第二类节点的真实玩法。
|
||||
2. 再收口商店系统 `P1-03 ~ P1-04`,把“买/卖/定价”统一到同一套库存与价格规则中。
|
||||
3. 之后补运行中长期状态承载:道具系统 `P1-05` 与继续挑战 `P1-06`。
|
||||
4. 再做主题玩法底座与两个主题规则 `P1-07 ~ P1-09`,避免火山、山地各自临时写一套。
|
||||
5. 最后做大关后主题选择 `P1-10`,因为它依赖新的 run 分段结构与主题状态流转。
|
||||
|
||||
2. `TagRegistryComponent`
|
||||
- 统一作为 Tag 规则注册与重载入口。
|
||||
- 负责刷新:
|
||||
- `TagDefinitionRegistry`
|
||||
- `TagGenerationRuleRegistry`
|
||||
- `RarityTagBudgetRuleRegistry`
|
||||
- 替代旧的 `ProcedurePreload` 直连刷新逻辑,并收口当前主流程初始化入口。
|
||||
## 开工前置
|
||||
|
||||
### 不适合抽成 `GameFrameworkComponent` 的模块
|
||||
### A1. 先补 M2 状态承载
|
||||
|
||||
- `ComponentTagGenerationService`
|
||||
- `TowerTagAggregationService`
|
||||
- `TagEffectResolver`
|
||||
- `NumericTagEffectHandler`
|
||||
- `AttackShapeTagEffectHandler`
|
||||
- `EnemyStatusTagRegistry`
|
||||
- 当前状态:已完成。
|
||||
- 目标:在不破坏 M1 固定 10 节点闭环的前提下,为 M2 新功能预留明确状态字段。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs`
|
||||
- `Assets/GameMain/Scripts/Procedure/ProcedureMain/` 下相关推进服务
|
||||
- 具体工作:
|
||||
- 给 `RunState` 增加继续挑战层数、主题阶段索引、当前主题池、运行中道具列表或等价结构。
|
||||
- 明确哪些状态属于 `RunState`,哪些属于 `PlayerInventory`,避免“金币在 run、道具在 inventory、主题选择在 UI 临时变量”这种分散写法。
|
||||
- 为节点完成事件补足 M2 需要的快照字段,保证事件/商店/战斗都能通过统一推进入口回写状态。
|
||||
- 验收补充:
|
||||
- 不影响现有 M1 测试。
|
||||
- 新字段默认值明确,未启用 M2 功能时不会改变 M1 行为。
|
||||
|
||||
这些都保留为普通领域模块,因为它们本质上是规则计算、数据聚合或战斗解析,不需要全局生命周期。
|
||||
### A2. 为 M2 先补最小测试底座
|
||||
|
||||
## 目标模块结构
|
||||
- 当前状态:已完成。
|
||||
- 目标:先把容易回归的规则放进 `EditMode`,避免事件和定价后面越改越散。
|
||||
- 交付物:
|
||||
- `Assets/Tests/EditMode/`
|
||||
- 具体工作:
|
||||
- 先建三组测试入口:事件需求/效果执行、商店定价、继续挑战倍率。
|
||||
- 对主题玩法先补纯规则测试,场景表现与路径交互后续再补 PlayMode。
|
||||
- 验收补充:
|
||||
- 至少保证新增核心公式与随机种子行为可回归。
|
||||
|
||||
### 组件层
|
||||
## P1 详细计划
|
||||
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/TagRegistry/TagRegistryComponent.cs`
|
||||
### P1-01 事件节点系统(选项、概率、奖励/惩罚执行器)
|
||||
|
||||
### 领域模块层
|
||||
- 核心目标:把现有“能打开事件 UI”补完成“能稳定结算并推进节点”的正式系统。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Game/UseCase/EventFormUseCase.cs`
|
||||
- `Assets/GameMain/Scripts/Definition/Event/`
|
||||
- 必要时新增 `Assets/GameMain/Scripts/Procedure/` 下的事件结算服务
|
||||
- 具体拆分:
|
||||
- 增加统一事件执行入口:`检查 Requirements -> 执行 CostEffects -> 掷概率 -> 执行 RewardEffects -> 生成结算结果 -> EndEvent`。
|
||||
- 概率结算改为基于 `runSeed + sequenceIndex + eventId + optionIndex` 的稳定随机,避免同局回放不一致。
|
||||
- Requirements 不满足时,UI 侧需要明确禁止点击或提示原因,不能点了再静默失败。
|
||||
- Effects 不要散落在 UI 控制器里,统一走一个事件执行服务或等价显式流程函数。
|
||||
- 明确失败语义:要求不足属于“不可选”,概率失败属于“已执行但收益为空或惩罚生效”。
|
||||
- 验收补充:
|
||||
- 可稳定配置并执行至少 10 个事件。
|
||||
- 事件结束后金币、库存、耐久等变化能正确写回节点完成快照。
|
||||
- 同一种子重复运行时,事件随机结果一致。
|
||||
|
||||
- `Assets/GameMain/Scripts/Factory/ComponentItemFactory.cs`
|
||||
- 唯一负责“从配置行构造 `TowerCompItemData`”。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs`
|
||||
- 统一承载商店 / 掉落 / 奖励候选的随机合同。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs`
|
||||
- 只负责商店货物生成。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs`
|
||||
- 只负责掉落池筛选、权重抽样、稀有度曲线。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs`
|
||||
- 只负责 3 选 1 奖励候选生成。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatSettlementCalculator.cs`
|
||||
- 只负责结算计算。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatSettlementCommitter.cs`
|
||||
- 只负责把结算结果提交到玩家库存。
|
||||
### P1-02 落地 3 个示例事件(赌马 / 组件交换 / 耐久换金币)
|
||||
|
||||
## 现有职责迁移目标
|
||||
- 核心目标:用 3 个真实事件验证 `P1-01` 的执行器不是一次性硬编码。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/DataTables/Event.txt`
|
||||
- 若现有效果不足,补 `Assets/GameMain/Scripts/Definition/Event/EventEffect/`
|
||||
- 若现有条件不足,补 `Assets/GameMain/Scripts/Definition/Event/EventRequirement/`
|
||||
- 具体拆分:
|
||||
- 保留并收口“赌马”事件,验证 `GoldAtLeast + AddGold + Probability`。
|
||||
- 把“组件交换”改成真正符合设计的链路:支付指定品质组件,返回不低于原品质的新组件;现有“换金币”样例不足以覆盖该目标。
|
||||
- 新增“耐久换金币”事件,复用当前耐久体系,明确扣减对象、扣减数量、损坏后的处理与节点结束后的清理。
|
||||
- 事件表扩充到 10 个可用事件时,先按低风险/中风险/功能型三类分层,避免全是纯金币事件。
|
||||
- 验收补充:
|
||||
- 三个示例事件在局内都能完整触发、结算、回写状态。
|
||||
- 不会出现组件被扣掉但奖励未发、或耐久扣成负值这种半成功状态。
|
||||
|
||||
### 商店链路
|
||||
### P1-03 商店节点:购买 / 出售组件
|
||||
|
||||
- `ShopNodeComponent`
|
||||
- 保留“节点进入 / 退出 / 发事件 / 打开 UI”职责。
|
||||
- `ShopFormUseCase`
|
||||
- 删除组件随机生成职责。
|
||||
- 保留“当前货物模型、购买行为、UI RawData 组装”职责。
|
||||
- 商店货物来源统一改为 `GameEntry.InventoryGeneration.BuildShopGoods(...)`。
|
||||
- 核心目标:把当前“仅购买组件”的商店补成完整交易节点。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/UI/Shop/`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs`
|
||||
- 必要时新增 `Assets/GameMain/Scripts/Utility/` 下的交易规则工具
|
||||
- 具体拆分:
|
||||
- Shop UI 增加玩家库存展示与出售入口,至少支持出售三类组件。
|
||||
- 交易结算统一走一个买卖接口,不要在 UI 层分别修改金币和库存。
|
||||
- 商品购买后状态、库存变化、金币变化要同时刷新,不允许 UI 还显示可买但实际已买。
|
||||
- 明确商店节点退出条件与节点完成时机,避免出现开店即完成或出售后不回流的问题。
|
||||
- 验收补充:
|
||||
- 买卖后库存与金币实时正确更新。
|
||||
- 关闭商店后重新打开同一节点时,商品状态与本节点交易结果一致。
|
||||
|
||||
### 战斗掉落链路
|
||||
### P1-04 商店定价规则:买价、半价回收、卖塔 +10%、耐久折价
|
||||
|
||||
- `CombatScheduler`
|
||||
- 不再直接持有复杂掉落生成细节。
|
||||
- 只在敌人死亡时调用 `GameEntry.InventoryGeneration.ResolveEnemyDrop(...)`。
|
||||
- 旧 `EnemyDropResolver`
|
||||
- 已从主链路移除,并已删除旧实现文件。
|
||||
- 其职责已拆给:
|
||||
- `DropPoolRoller`
|
||||
- `ComponentItemFactory`
|
||||
- `RewardCandidateBuilder`
|
||||
- 核心目标:把价格从“按品质随机区间”升级为可解释、可测试的公式。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/Utility/`
|
||||
- `Assets/GameMain/Scripts/Entity/` 或库存相关域对象
|
||||
- 视需要调整 `Assets/GameMain/DataTables/ShopPrice.txt`
|
||||
- 具体拆分:
|
||||
- 抽出统一价格解析入口:基础买价、出售价、塔出售加成、耐久折价都走同一公式服务。
|
||||
- 明确“组件价格”和“整塔价格”关系,不能在 UI 里临时把三组件价格相加后再乘一个魔法数。
|
||||
- 明确耐久折价规则:按当前耐久比例折价,还是按损坏阈值分段折价;先写死规则文档,再落代码。
|
||||
- 如果 `ShopPrice.txt` 只够表达品质区间,新增字段或新表承载倍率,不要继续在代码里硬编码。
|
||||
- 验收补充:
|
||||
- 买价、回收半价、卖塔 +10%、耐久折价都可通过测试直接验证。
|
||||
- 同类物品在同一规则下价格可解释,不出现随机漂移。
|
||||
|
||||
### 战斗结算链路
|
||||
### P1-05 道具系统(影响初始金币、掉金倍率等)
|
||||
|
||||
- `CombatSettlementService`
|
||||
- 从混合类拆成薄流程壳。
|
||||
- 保留最少流程编排。
|
||||
- 计算逻辑迁到 `CombatSettlementCalculator`。
|
||||
- 库存提交逻辑迁到 `CombatSettlementCommitter`。
|
||||
- 奖励候选生成改为调用 `GameEntry.InventoryGeneration.BuildRewardCandidates(...)`。
|
||||
- 核心目标:引入 run 内长期生效的被动效果,支撑中期构筑深度。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/Entity/`
|
||||
- `Assets/GameMain/DataTables/*.txt`
|
||||
- `Assets/GameMain/Scripts/Procedure/ProcedureMain/`
|
||||
- 具体拆分:
|
||||
- 先定义“道具”最小模型:唯一 Id、名称、描述、叠加规则、效果列表。
|
||||
- 道具效果统一挂在 run 状态,不要把“初始金币加成”写进开局逻辑、“掉金倍率”写进敌人掉落逻辑、“商店折扣”又写进商店 UI。
|
||||
- 第一批只做能复用现有链路的效果:初始金币、掉金倍率、商店折扣、额外奖励候选、事件收益倍率。
|
||||
- 明确获取来源:先只接战斗奖励或事件奖励,暂不同时开启商店售卖与局外解锁。
|
||||
- 验收补充:
|
||||
- 至少 5 个道具可叠加生效。
|
||||
- 多个道具同时存在时,叠加顺序和公式固定且可测试。
|
||||
|
||||
### Tag 初始化链路
|
||||
### P1-06 战斗后“继续挑战”机制(高强度高爆率)
|
||||
|
||||
- `ProcedurePreload`
|
||||
- 移除 Tag 注册表刷新细节。
|
||||
- 只保留资源与数据表预加载职责。
|
||||
- `ProcedureMain`
|
||||
- 进入主流程时主动调用 `GameEntry.TagRegistry.OnInit()`。
|
||||
- `TagRegistryComponent`
|
||||
- 成为 Tag 相关表与注册表的唯一运行时入口。
|
||||
- 核心目标:把当前战斗结算从单向结束改成“收手 / 继续”的风险收益选择。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/Procedure/`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/`
|
||||
- 需要时补 `Assets/GameMain/Scripts/UI/Combat/` 或现有结算 UI
|
||||
- 具体拆分:
|
||||
- 战斗胜利后新增继续挑战分支,不改变失败结算语义。
|
||||
- 给 run 增加当前节点继续挑战层数,并把它传入敌人强度和掉落倍率计算。
|
||||
- 强度提升与爆率提升必须都走显式公式,且支持按层数递增。
|
||||
- 继续挑战退出时,节点奖励如何结算、是否覆盖原奖励、是否保底,需要在实现前先写清楚。
|
||||
- 验收补充:
|
||||
- 选择继续后敌人强度明显提升且爆率提升。
|
||||
- 连续多次继续时,奖励和风险曲线都可感知,不出现“白打”。
|
||||
|
||||
## 分阶段执行
|
||||
### P1-07 火山主题规则(高温、岩浆格、抗火敌人)
|
||||
|
||||
## 阶段 G1 - 收口组件产出入口
|
||||
- 核心目标:做出第一个真实改变战斗结果的主题规则,而不是只换关卡皮肤。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/Scene/`
|
||||
- `Assets/GameMain/Scripts/Entity/`
|
||||
- 必要时新增主题规则配置表
|
||||
- 具体拆分:
|
||||
- 先抽主题战斗规则入口,统一在战斗调度或关卡运行时挂载主题效果。
|
||||
- 高温规则先定义为全局环境修正;岩浆格是地图局部修正;敌人抗火是单位侧修正,三者不要混成一个大脚本。
|
||||
- 岩浆格必须有清晰表现和可命中逻辑,不能只有视觉特效。
|
||||
- 与现有 Tag / 属性体系冲突处优先显式列出,不在第一版做复杂联动。
|
||||
- 验收补充:
|
||||
- 岩浆效果可视,且确实会改变塔或敌人的战斗结果。
|
||||
|
||||
| 状态 | ID | 任务 | 目标 |
|
||||
|-----|-------|-----------------------------------|------------|
|
||||
| [x] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 |
|
||||
| [x] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 |
|
||||
| [x] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 |
|
||||
| [x] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 |
|
||||
### P1-08 山地主题规则(高度、悬崖、位移致死)
|
||||
|
||||
### G1 验收标准
|
||||
- 核心目标:做出第二个与火山玩法差异明显的主题。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/Scene/`
|
||||
- `Assets/GameMain/Scripts/Entity/`
|
||||
- 必要时新增高度或地形标记配置
|
||||
- 具体拆分:
|
||||
- 为地图格子补高度/悬崖属性,而不是把高度写死在某一张图脚本里。
|
||||
- 塔攻击高打低、低打高的距离或命中修正单独实现,避免和基础攻击范围直接耦死。
|
||||
- 敌人上下坡移速修正与悬崖击退致死分开结算,便于后续加其他位移效果。
|
||||
- 先支持对现有敌人和击退能力最小接入,不追求一次做完整地形系统。
|
||||
- 验收补充:
|
||||
- 高低地形会影响攻防与移速。
|
||||
- 悬崖击退击杀能真实生效并参与结算。
|
||||
|
||||
- 商店仍可正常打开、购买、加背包。
|
||||
- 商店不再直接读取组件表并构造 `Muzzle/Bearing/Base` 实例。
|
||||
- 商店所有组件实例都通过同一工厂生成。
|
||||
### P1-09 主题地图掉落偏好(按主题偏置组件)
|
||||
|
||||
## 阶段 G2 - 收口战斗掉落与奖励候选
|
||||
- 核心目标:让主题不只影响战斗规则,也影响构筑倾向。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/DataTables/*.txt`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/`
|
||||
- 具体拆分:
|
||||
- 在现有 `OutGameDropPool` 或新增偏置表中表达主题掉落权重,不要把火山/山地偏置硬编码在掉落代码里。
|
||||
- 主题偏置只影响权重,不直接写死掉落结果,保留随机性。
|
||||
- 明确偏置作用对象:组件槽位、品质、Tag、或其组合,第一版建议先偏置组件/Tag。
|
||||
- 验收补充:
|
||||
- 不同主题统计掉落分布显著不同。
|
||||
- 偏置结果与 `P1-07 / P1-08` 的主题反制方向一致。
|
||||
|
||||
| 状态 | ID | 任务 | 目标 |
|
||||
|-----|-------|----------------------------------------|---------------|
|
||||
| [x] | G2-01 | 新增 `DropPoolRoller` | 收口掉落池与稀有度抽样 |
|
||||
| [x] | G2-02 | 新增 `RewardCandidateBuilder` | 收口 3 选 1 候选生成 |
|
||||
| [x] | G2-03 | 让战斗掉落改走 `InventoryGenerationComponent` | 战斗不再自己构造组件 |
|
||||
| [x] | G2-04 | 让满血奖励候选改走统一入口 | 掉落与奖励候选共用产出体系 |
|
||||
### P1-10 大关完成后可选下一主题地图
|
||||
|
||||
### G2 验收标准
|
||||
- 核心目标:把当前“固定单主题 10 节点 run”扩成“按大关推进并选择下个主题”。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/Procedure/`
|
||||
- `Assets/GameMain/Scripts/UI/Templates/GameScene/` 或现有节点地图相关 UI
|
||||
- 具体拆分:
|
||||
- 先定义“大关”与“节点序列”的关系:是每个大关 10 节点,还是一个 run 内包含多个主题段。
|
||||
- Run 流程需要新增“主题选择阶段”,不能继续只靠 `CurrentNodeIndex++` 线性推进。
|
||||
- 主题选择 UI 至少展示 2 个候选主题、主题效果摘要、预期掉落倾向。
|
||||
- 选择后要能重建后续节点池、关卡池和掉落偏置,而不是只改一个 `ThemeType` 字段。
|
||||
- 验收补充:
|
||||
- 通关后至少可在 2 个主题间选择。
|
||||
- 主题选择会真实影响后续关卡、掉落和规则。
|
||||
|
||||
- 敌人死亡后仍能正常掉 coin、gold、组件。
|
||||
- 满血奖励 3 选 1 仍能正常打开并选择。
|
||||
- 掉落与奖励候选不再重复维护组件实例构造逻辑。
|
||||
- `CombatScheduler` 与 `CombatSettlementService` 已改为统一调用 `GameEntry.InventoryGeneration`。
|
||||
## 推荐分批提交
|
||||
|
||||
## 阶段 G3 - 收口结算模块边界
|
||||
### 批次 1:事件系统收口
|
||||
|
||||
| 状态 | ID | 任务 | 目标 |
|
||||
|-----|-------|---------------------------------|-----------------|
|
||||
| [x] | G3-01 | 新增 `CombatSettlementCalculator` | 只保留结算计算 |
|
||||
| [x] | G3-02 | 新增 `CombatSettlementCommitter` | 只保留库存提交 |
|
||||
| [x] | G3-03 | 精简 `CombatSettlementService` | 只剩流程编排 |
|
||||
| [x] | G3-04 | 清理奖励 RawData 重复映射 | 候选生成与 UI 组装边界清晰 |
|
||||
- `A1` 中与事件相关的状态补充
|
||||
- `P1-01`
|
||||
- `P1-02`
|
||||
|
||||
### G3 验收标准
|
||||
### 批次 2:商店系统收口
|
||||
|
||||
- 战斗结算金币、奖励库存、耐久扣减结果保持不变。
|
||||
- 满血奖励选择后仍能正确并包。
|
||||
- `CombatSettlementService` 不再同时承担计算、提交、候选生成三类职责。
|
||||
- `P1-03`
|
||||
- `P1-04`
|
||||
|
||||
## 阶段 G4 - 收口 Tag 初始化入口
|
||||
### 批次 3:run 深度状态
|
||||
|
||||
| 状态 | ID | 任务 | 目标 |
|
||||
|-----|-------|----------------------------------|--------------------|
|
||||
| [x] | G4-01 | 新增 `TagRegistryComponent` | 建立 Tag 运行时入口 |
|
||||
| [x] | G4-02 | 挪出 `ProcedurePreload` 中 Tag 注册逻辑 | 流程代码不再直接维护 Tag 注册表 |
|
||||
| [x] | G4-03 | 对齐 Tag 加载时机与重载入口 | 初始化路径明确 |
|
||||
- `P1-05`
|
||||
- `P1-06`
|
||||
|
||||
### G4 验收标准
|
||||
### 批次 4:主题玩法底座与内容
|
||||
|
||||
- `Tag.txt`、`TagConfig.txt`、`RarityTagBudget.txt` 加载后仍能正确刷新运行时。
|
||||
- Tag 展示、Tag 生成、Tag 战斗效果不受影响。
|
||||
- Tag 相关运行时入口不再散落在预加载流程里。
|
||||
- 主题规则统一入口
|
||||
- `P1-07`
|
||||
- `P1-08`
|
||||
- `P1-09`
|
||||
|
||||
## 阶段 G5 - 收口随机合同
|
||||
### 批次 5:多主题推进
|
||||
|
||||
| 状态 | ID | 任务 | 目标 |
|
||||
|-----|-------|------------------------|---------------------|
|
||||
| [x] | G5-01 | 统一商店 / 掉落 / 奖励候选的随机上下文 | 全部产出链路使用一致合同 |
|
||||
| [x] | G5-02 | 补齐整体产出的 `runSeed` 稳定性 | 不只稳定 Tag,也稳定物品产出 |
|
||||
| [x] | G5-03 | 增加对应 EditMode 测试 | 同 Run 可复现,跨 Run 可区分 |
|
||||
- `P1-10`
|
||||
|
||||
### G5 验收标准
|
||||
## 当前建议的第一开工项
|
||||
|
||||
- 同一 `runSeed + sequenceIndex` 下,商店货物、掉落候选、奖励候选可复现。
|
||||
- 不同 `runSeed` 下,产出结果可区分。
|
||||
- 不再出现“Tag 稳定但物品本体不稳定”的混合口径。
|
||||
|
||||
## 具体迁移清单
|
||||
|
||||
### 第一批直接改动文件
|
||||
|
||||
- `Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs`
|
||||
- `Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs`
|
||||
- `Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs`
|
||||
- `Assets/GameMain/Scripts/Base/GameEntry.Custom.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs`
|
||||
|
||||
### 第二批新增文件
|
||||
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/TagRegistry/TagRegistryComponent.cs`
|
||||
- `Assets/GameMain/Scripts/Factory/ComponentItemFactory.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatSettlementCalculator.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatSettlementCommitter.cs`
|
||||
|
||||
## 本次重构的限制
|
||||
|
||||
- 不在这一轮扩展新的 Tag 功能。
|
||||
- 不在这一轮扩展新的商店玩法或新的奖励 UI 形态。
|
||||
- 不为了“统一”而把所有领域逻辑都塞进 `GameFrameworkComponent`。
|
||||
- 不改动现有 UI 五层结构以外的职责边界。
|
||||
|
||||
## 当前推荐执行顺序
|
||||
|
||||
1. 先做 `InventoryGenerationComponent + ComponentItemFactory`。
|
||||
2. 先替换商店货物生成。
|
||||
3. 再替换战斗掉落与奖励候选生成。
|
||||
4. 再拆战斗结算计算与提交。
|
||||
5. 最后收 `TagRegistryComponent` 与主流程中的 Tag 初始化入口。
|
||||
|
||||
## 通过标准
|
||||
|
||||
- 组件实例生成入口只保留一套。
|
||||
- 掉落、商店、奖励候选不再各自维护相似逻辑。
|
||||
- Tag 模块保留现有分层,不再继续和流程代码缠在一起。
|
||||
- `GameFrameworkComponent` 只承接运行时入口,不承接纯规则实现。
|
||||
|
||||
## 当前结果
|
||||
|
||||
- `InventoryGenerationComponent` 已成为商店、战斗掉落、奖励候选的统一运行时入口。
|
||||
- 掉落概率规则与掉落物品构造已继续下沉到 `OutGameDropRuleService` 与 `OutGameDropItemBuilder`,`InventoryGenerationComponent` 保持入口编排职责。
|
||||
- `TagRegistryComponent` 已成为 Tag 运行时入口,并由 `ProcedureMain` 主动初始化。
|
||||
- `TagRegistryComponent` 已改为 fail-fast 初始化,缺少必要数据表时会直接暴露错误。
|
||||
- `InventoryGenerationRandomContext` 已统一商店、掉落、奖励候选的随机合同,并补齐稳定临时实例 Id。
|
||||
- `Assets/Tests/EditMode/InventoryGenerationStabilityTests.cs` 已覆盖 G5 的可复现性验收点。
|
||||
1. 立刻收口 `P1-01`,把 `EventFormUseCase.SelectOption()` 从占位实现改成正式结算。
|
||||
2. 基于 A2 里已经补好的事件定义测试入口,新增事件执行链路测试,覆盖 requirement、cost、probability、reward 和完成快照回写。
|
||||
3. 用 `P1-02` 的三个示例事件验证执行器能力,再扩表到 10 个事件。
|
||||
4. 之后再进入商店买卖与定价,不建议现在同时并行推进主题地图。
|
||||
|
|
|
|||
Loading…
Reference in New Issue