This commit is contained in:
2026-03-13 17:58:09 +08:00
parent 777c58b812
commit c76fd85a2c
25 changed files with 1214 additions and 360 deletions
@@ -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;
}
}
}
+129 -19
View File
@@ -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: