S4-07 process 2

当前 `Tag.txt -> DRTag -> TagGenerationRuleRegistry` 已形成组件 Tag 生成规则闭环:
- `InventoryTagRuleService` 不再主要依赖内部 `MinRarity` 硬编码,而是统一消费 `DRTag` 提供的 `MinRarity + Weight`
- `ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 也已切回这一统一入口。
- 现阶段的职责边界明确为:`Tag.txt` 负责基础字典与生成规则,`TagConfig.txt` 负责触发阶段、描述与效果参数。
This commit is contained in:
SepComet
2026-03-11 11:20:16 +08:00
parent 9de2e50262
commit b1b68ebde5
46 changed files with 615 additions and 452 deletions
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class FixedRunNodeSequenceBuilder
{
private const int FixedNodeCount = 10;
private static readonly int[] PlainCombatLevelCycle = { 1, 2, 3, 1 };
public static IReadOnlyList<RunNodeSeed> Build(LevelThemeType themeType)
{
if (themeType != LevelThemeType.Plain)
{
return new List<RunNodeSeed>();
}
List<RunNodeSeed> nodeSeeds = new List<RunNodeSeed>(FixedNodeCount)
{
CreateNodeSeed(1, 0, RunNodeType.Combat, themeType, PlainCombatLevelCycle[0]),
CreateNodeSeed(2, 1, RunNodeType.Event, themeType, 0),
CreateNodeSeed(3, 2, RunNodeType.Combat, themeType, PlainCombatLevelCycle[1]),
CreateNodeSeed(4, 3, RunNodeType.Shop, themeType, 0),
CreateNodeSeed(5, 4, RunNodeType.Combat, themeType, PlainCombatLevelCycle[2]),
CreateNodeSeed(6, 5, RunNodeType.Event, themeType, 0),
CreateNodeSeed(7, 6, RunNodeType.Combat, themeType, PlainCombatLevelCycle[3]),
CreateNodeSeed(8, 7, RunNodeType.Shop, themeType, 0),
CreateNodeSeed(9, 8, RunNodeType.Event, themeType, 0),
CreateNodeSeed(10, 9, RunNodeType.BossCombat, themeType, 4)
};
return nodeSeeds;
}
private static RunNodeSeed CreateNodeSeed(
int nodeId,
int sequenceIndex,
RunNodeType nodeType,
LevelThemeType themeType,
int linkedLevelId)
{
return new RunNodeSeed
{
NodeId = nodeId,
SequenceIndex = sequenceIndex,
NodeType = nodeType,
ThemeType = themeType,
LinkedLevelId = linkedLevelId
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 650d0ca3526d4b3db552d70292267456
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,426 @@
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.Procedure
{
public class ProcedureMain : ProcedureBase
{
public override bool UseNativeDialog => false;
private RepoFormUseCase _repoFormUseCase;
private NodeMapFormUseCase _nodeMapFormUseCase;
private RunState _currentRunState;
private ProcedureMainFlowPhase _flowPhase = ProcedureMainFlowPhase.Hub;
private bool _isRunCompleteDialogShown;
private bool _isReturnToMenuPending;
#region FSM
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
{
base.OnEnter(procedureOwner);
GameEntry.Event.Subscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
GameEntry.EventNode.OnInit();
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
GameEntry.ShopNode.OnInit();
GameEntry.PlayerInventory?.OnInit();
_currentRunState = RunStateFactory.CreateFixedRun(
LevelThemeType.Plain,
GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: null);
_repoFormUseCase = new RepoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
_nodeMapFormUseCase = new NodeMapFormUseCase();
_nodeMapFormUseCase.SetRunState(_currentRunState);
GameEntry.UIRouter.BindUIUseCase(UIFormType.NodeMapForm, _nodeMapFormUseCase);
_isRunCompleteDialogShown = false;
_isReturnToMenuPending = false;
EnterHubFlow();
}
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds,
float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (_isReturnToMenuPending)
{
_isReturnToMenuPending = false;
procedureOwner.SetData<VarInt32>("NextSceneId", (int)SceneType.Menu);
ChangeState<ProcedureChangeScene>(procedureOwner);
return;
}
GameEntry.CombatNode.OnUpdate(elapseSeconds, realElapseSeconds);
}
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
{
GameEntry.CombatNode.OnShutdown();
GameEntry.Event.Unsubscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
GameEntry.Event.Unsubscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Unsubscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
GameEntry.UIRouter.CloseUI(UIFormType.RepoForm);
GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
_repoFormUseCase = null;
_nodeMapFormUseCase = null;
_currentRunState = null;
_flowPhase = ProcedureMainFlowPhase.Hub;
_isRunCompleteDialogShown = false;
_isReturnToMenuPending = false;
base.OnLeave(procedureOwner, isShutdown);
}
#endregion
private void OnNodeEnter(object sender, GameEventArgs e)
{
if (!(e is NodeEnterEventArgs args))
{
return;
}
if (!string.IsNullOrWhiteSpace(args.RunId) &&
_currentRunState != null &&
!string.Equals(args.RunId, _currentRunState.RunId))
{
Log.Warning(
"ProcedureMain.OnNodeEnter() ignored. EventRunId={0}, CurrentRunId={1}.",
args.RunId,
_currentRunState.RunId);
return;
}
RunNodeState currentNode = _currentRunState?.CurrentNode;
if (!ProcedureMainNodeEventGuardService.MatchesCurrentNode(
_currentRunState,
args.NodeId,
args.NodeType,
args.SequenceIndex))
{
Log.Warning(
"ProcedureMain.OnNodeEnter() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.",
args.NodeId,
args.NodeType,
args.SequenceIndex,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1);
return;
}
Log.Info(
"ProcedureMain.OnNodeEnter() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.",
string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId,
args.NodeId,
args.NodeType,
args.SequenceIndex);
EnterNodeFlow();
}
private void OnNodeComplete(object sender, GameEventArgs e)
{
if (!(e is NodeCompleteEventArgs args))
{
return;
}
if (!string.IsNullOrWhiteSpace(args.RunId) &&
_currentRunState != null &&
!string.Equals(args.RunId, _currentRunState.RunId))
{
Log.Warning(
"ProcedureMain.OnNodeComplete() ignored. EventRunId={0}, CurrentRunId={1}.",
args.RunId,
_currentRunState.RunId);
return;
}
RunNodeState currentNode = _currentRunState?.CurrentNode;
if (!ProcedureMainNodeEventGuardService.MatchesCurrentNode(
_currentRunState,
args.NodeId,
args.NodeType,
args.SequenceIndex))
{
Log.Warning(
"ProcedureMain.OnNodeComplete() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.",
args.NodeId,
args.NodeType,
args.SequenceIndex,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1);
return;
}
Log.Info(
"ProcedureMain.OnNodeComplete() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, CompletionStatus={4}, CombatWon={5}.",
string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId,
args.NodeId,
args.NodeType,
args.SequenceIndex,
args.CompletionStatus,
args.CombatWon);
BackpackInventoryData snapshot = args.InventorySnapshotAfterNode;
if (snapshot == null && GameEntry.PlayerInventory != null)
{
snapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
}
HandleRunAdvanceResult(
ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, snapshot));
}
private void OnNodeMapNodeEnterRequested(object sender, GameEventArgs e)
{
if (!(sender is NodeMapForm) || !(e is NodeMapNodeEnterRequestedEventArgs args))
{
return;
}
RunNodeState currentNode = _currentRunState?.CurrentNode;
if (_currentRunState == null ||
_flowPhase != ProcedureMainFlowPhase.Hub ||
!_currentRunState.CanEnterCurrentNode ||
currentNode == null)
{
Log.Warning(
"ProcedureMain.OnNodeMapNodeEnterRequested() ignored. FlowPhase={0}, HasEnterableNode={1}.",
_flowPhase,
_currentRunState != null && _currentRunState.CanEnterCurrentNode);
return;
}
if (args.SequenceIndex != currentNode.SequenceIndex || args.NodeType != currentNode.NodeType)
{
Log.Warning(
"ProcedureMain.OnNodeMapNodeEnterRequested() ignored. Requested={0}#{1}, CurrentNode={2}#{3}.",
args.NodeType,
args.SequenceIndex,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
}
switch (currentNode.NodeType)
{
case RunNodeType.Combat:
case RunNodeType.BossCombat:
ProcedureMainCombatEntryValidationResult validationResult =
ProcedureMainCombatEntryValidationService.Validate(
GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: null);
if (!validationResult.CanEnterCombat)
{
LogCombatEntryBlocked(currentNode, validationResult);
OpenBlockedCombatDialog(validationResult);
return;
}
GameEntry.CombatNode.StartCombat(
currentNode.LinkedLevelId,
_currentRunState.RunId,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
case RunNodeType.Event:
GameEntry.EventNode.StartEvent(
_currentRunState.RunId,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
case RunNodeType.Shop:
GameEntry.ShopNode.StartShop(
_currentRunState.RunId,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
default:
Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.",
currentNode.NodeType);
return;
}
}
private void OpenBlockedCombatDialog(ProcedureMainCombatEntryValidationResult validationResult)
{
GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
GameEntry.UIRouter.OpenUI(
UIFormType.DialogForm,
ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData(validationResult));
}
private void LogCombatEntryBlocked(
RunNodeState currentNode,
ProcedureMainCombatEntryValidationResult validationResult)
{
switch (validationResult?.BlockReason)
{
case ProcedureMainCombatEntryBlockReason.InventoryUnavailable:
Log.Warning(
"ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason={4}.",
_currentRunState?.RunId,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1,
ProcedureMainCombatEntryBlockReason.InventoryUnavailable);
return;
case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower:
Log.Warning(
"ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason={4}, InvalidParticipantTowers={5}.",
_currentRunState?.RunId,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1,
ProcedureMainCombatEntryBlockReason.NoValidParticipantTower,
ProcedureMainCombatEntryValidationService.BuildInvalidParticipantTowerLog(
validationResult.ValidationSummary));
return;
default:
Log.Warning(
"ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason=Unknown.",
_currentRunState?.RunId,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1);
return;
}
}
private void HandleRunAdvanceResult(ProcedureMainRunAdvanceResult result)
{
switch (result)
{
case ProcedureMainRunAdvanceResult.NoChange:
return;
case ProcedureMainRunAdvanceResult.NodeException:
Log.Info(
"ProcedureMain current node entered exception state. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.",
_currentRunState?.RunId,
_currentRunState?.CurrentNode?.NodeId ?? 0,
_currentRunState?.CurrentNode?.NodeType ?? RunNodeType.None,
_currentRunState?.CurrentNode?.SequenceIndex ?? -1);
EnterHubFlow();
return;
case ProcedureMainRunAdvanceResult.AdvancedToNextNode:
LogNextNode();
EnterHubFlow();
return;
case ProcedureMainRunAdvanceResult.RunCompleted:
EnterRunCompletedPendingFinish();
return;
default:
return;
}
}
private void EnterHubFlow()
{
_flowPhase = ProcedureMainFlowPhase.Hub;
_nodeMapFormUseCase?.SetRunState(_currentRunState);
GameEntry.UIRouter.OpenUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
}
private void EnterNodeFlow()
{
_flowPhase = ProcedureMainFlowPhase.NodeActive;
CloseHubUI();
}
private void EnterRunCompletedPendingFinish()
{
_flowPhase = ProcedureMainFlowPhase.RunCompletedPendingFinish;
_nodeMapFormUseCase?.SetRunState(_currentRunState);
CloseHubUI();
Log.Info("ProcedureMain run completed. RunId={0}", _currentRunState?.RunId);
ProcedureMainRunCompletionResult result =
ProcedureMainRunCompletionService.TryEnterCompletedPendingFinish(_isRunCompleteDialogShown);
if (result == ProcedureMainRunCompletionResult.ShowCompletionDialog)
{
_isRunCompleteDialogShown = true;
OpenRunCompleteDialog();
}
}
private void CloseHubUI()
{
GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
}
private void LogNextNode()
{
RunNodeState nextNode = _currentRunState?.CurrentNode;
if (nextNode == null)
{
return;
}
Log.Info(
"ProcedureMain advanced run. RunId={0}, NextNodeType={1}, SequenceIndex={2}, LevelId={3}.",
_currentRunState.RunId,
nextNode.NodeType,
nextNode.SequenceIndex,
nextNode.LinkedLevelId);
}
private void OpenRunCompleteDialog()
{
GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData
{
Mode = 1,
Title = "Run Complete",
Message = "Boss node completed. This run is finished and will return to the main menu.",
PauseGame = false,
ConfirmText = "Return to Menu",
OnClickConfirm = OnRunCompleteDialogConfirmed
});
}
private void OnRunCompleteDialogConfirmed(object userData)
{
_ = userData;
ProcedureMainRunCompletionResult result = ProcedureMainRunCompletionService.TryConfirmReturnToMenu(
_flowPhase,
_isReturnToMenuPending);
if (result != ProcedureMainRunCompletionResult.ReturnToMenu)
{
Log.Warning(
"ProcedureMain ignored run completion confirm. FlowPhase={0}, ReturnPending={1}.",
_flowPhase,
_isReturnToMenuPending);
return;
}
_isReturnToMenuPending = true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e6f092ddfab48b74d8e8cc338e4742e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,13 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public sealed class ProcedureMainCombatEntryValidationResult
{
public bool CanEnterCombat => BlockReason == ProcedureMainCombatEntryBlockReason.None;
public ProcedureMainCombatEntryBlockReason BlockReason { get; set; }
public CombatParticipantTowerValidationSummary ValidationSummary { get; set; }
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5699aa9933294974875bfce1d34d1d4b
timeCreated: 1773198121
@@ -0,0 +1,140 @@
using System.Text;
using GeometryTD.Definition;
using GeometryTD.UI;
namespace GeometryTD.Procedure
{
public static class ProcedureMainCombatEntryValidationService
{
public static ProcedureMainCombatEntryValidationResult Validate(BackpackInventoryData inventory)
{
if (inventory == null)
{
return new ProcedureMainCombatEntryValidationResult
{
BlockReason = ProcedureMainCombatEntryBlockReason.InventoryUnavailable,
ValidationSummary = new CombatParticipantTowerValidationSummary()
};
}
CombatParticipantTowerValidationSummary summary =
CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory);
return new ProcedureMainCombatEntryValidationResult
{
BlockReason = summary.HasAnyValidParticipantTower
? ProcedureMainCombatEntryBlockReason.None
: ProcedureMainCombatEntryBlockReason.NoValidParticipantTower,
ValidationSummary = summary
};
}
public static string BuildInvalidParticipantTowerLog(
CombatParticipantTowerValidationSummary summary)
{
if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return "none";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult result = summary.InvalidResults[i];
if (result == null)
{
continue;
}
if (builder.Length > 0)
{
builder.Append(", ");
}
builder.Append('#');
builder.Append(result.TowerInstanceId);
builder.Append(':');
builder.Append(result.FailureReason);
}
return builder.Length > 0 ? builder.ToString() : "none";
}
public static DialogFormRawData BuildBlockedCombatDialogRawData(
ProcedureMainCombatEntryValidationResult validationResult)
{
return new DialogFormRawData
{
Mode = 1,
Title = "无法进入战斗",
Message = BuildBlockedCombatDialogMessage(validationResult),
PauseGame = false,
ConfirmText = "知道了"
};
}
private static string BuildBlockedCombatDialogMessage(
ProcedureMainCombatEntryValidationResult validationResult)
{
if (validationResult == null)
{
return "当前无法确认出战信息,请稍后重试。";
}
switch (validationResult.BlockReason)
{
case ProcedureMainCombatEntryBlockReason.InventoryUnavailable:
return "当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。";
case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower:
return BuildNoValidParticipantTowerMessage(validationResult.ValidationSummary);
default:
return "当前出战校验未通过,暂时不能进入战斗。";
}
}
private static string BuildNoValidParticipantTowerMessage(
CombatParticipantTowerValidationSummary summary)
{
if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return "参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。";
}
StringBuilder builder = new StringBuilder();
builder.Append("参战区没有可出战的完整塔。");
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult result = summary.InvalidResults[i];
if (result == null)
{
continue;
}
builder.Append('\n');
builder.Append("塔 #");
builder.Append(result.TowerInstanceId);
builder.Append(' ');
builder.Append(GetFailureReasonMessage(result.FailureReason));
}
return builder.ToString();
}
private static string GetFailureReasonMessage(CombatParticipantTowerValidationFailureReason failureReason)
{
switch (failureReason)
{
case CombatParticipantTowerValidationFailureReason.TowerMissing:
return "已不存在,无法参战。";
case CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent:
return "缺少枪口组件。";
case CombatParticipantTowerValidationFailureReason.MissingBearingComponent:
return "缺少轴承组件。";
case CombatParticipantTowerValidationFailureReason.MissingBaseComponent:
return "缺少底座组件。";
default:
return "不满足当前参战条件。";
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: be3e90d1a1874e26af6d1555c301aa26
timeCreated: 1773198334
@@ -0,0 +1,22 @@
namespace GeometryTD.Procedure
{
public static class ProcedureMainNodeEventGuardService
{
public static bool MatchesCurrentNode(
RunState runState,
int nodeId,
RunNodeType nodeType,
int sequenceIndex)
{
RunNodeState currentNode = runState?.CurrentNode;
if (currentNode == null)
{
return false;
}
return (nodeId <= 0 || nodeId == currentNode.NodeId) &&
(nodeType == RunNodeType.None || nodeType == currentNode.NodeType) &&
(sequenceIndex < 0 || sequenceIndex == currentNode.SequenceIndex);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 212a0e12be2248bf9415a779414312a1
timeCreated: 1773198194
@@ -0,0 +1,26 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class ProcedureMainRunCompletionService
{
public static ProcedureMainRunCompletionResult TryEnterCompletedPendingFinish(bool isCompletionDialogShown)
{
return isCompletionDialogShown
? ProcedureMainRunCompletionResult.NoChange
: ProcedureMainRunCompletionResult.ShowCompletionDialog;
}
public static ProcedureMainRunCompletionResult TryConfirmReturnToMenu(
ProcedureMainFlowPhase flowPhase,
bool isReturnToMenuPending)
{
if (flowPhase != ProcedureMainFlowPhase.RunCompletedPendingFinish || isReturnToMenuPending)
{
return ProcedureMainRunCompletionResult.NoChange;
}
return ProcedureMainRunCompletionResult.ReturnToMenu;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8d565626cc594843989da76a623a1698
timeCreated: 1773198165
@@ -0,0 +1,27 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class ProcedureMainRunFlowService
{
public static ProcedureMainRunAdvanceResult TryAdvanceRun(
RunState runState,
RunNodeCompletionStatus completionStatus,
BackpackInventoryData snapshot)
{
if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, snapshot))
{
return ProcedureMainRunAdvanceResult.NoChange;
}
if (runState != null && runState.IsCompleted)
{
return ProcedureMainRunAdvanceResult.RunCompleted;
}
return completionStatus == RunNodeCompletionStatus.Exception
? ProcedureMainRunAdvanceResult.NodeException
: ProcedureMainRunAdvanceResult.AdvancedToNextNode;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f62dcb2d97fe4f0c884e6eab3d500edf
timeCreated: 1773198145
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using GeometryTD.CustomUtility;
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public enum RunNodeType
{
None = 0,
Combat = 1,
Event = 2,
Shop = 3,
BossCombat = 4
}
public enum RunNodeStatus
{
Locked = 0,
Available = 1,
Completed = 2,
Exception = 3,
Skipped = 4
}
public enum RunNodeCompletionStatus
{
None = 0,
Completed = 1,
Exception = 2
}
[Serializable]
public sealed class RunNodeSeed
{
public int NodeId { get; set; }
public RunNodeType NodeType { get; set; }
public LevelThemeType ThemeType { get; set; }
public int LinkedLevelId { get; set; }
public int SequenceIndex { get; set; } = -1;
}
[Serializable]
public sealed class RunNodeState
{
public int NodeId { get; internal set; }
public RunNodeType NodeType { get; internal set; }
public LevelThemeType ThemeType { get; internal set; }
public int LinkedLevelId { get; internal set; }
public RunNodeStatus Status { get; internal set; }
public int SequenceIndex { get; internal set; }
internal RunNodeState Clone()
{
return new RunNodeState
{
NodeId = NodeId,
NodeType = NodeType,
ThemeType = ThemeType,
LinkedLevelId = LinkedLevelId,
Status = Status,
SequenceIndex = SequenceIndex
};
}
}
[Serializable]
public sealed class RunState
{
private readonly List<RunNodeState> _nodes;
internal RunState(
string runId,
LevelThemeType themeType,
List<RunNodeState> nodes,
BackpackInventoryData runInventorySnapshot)
{
RunId = string.IsNullOrWhiteSpace(runId) ? Guid.NewGuid().ToString("N") : runId;
ThemeType = themeType;
_nodes = nodes ?? new List<RunNodeState>();
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(runInventorySnapshot);
CurrentNodeIndex = _nodes.Count > 0 ? 0 : -1;
IsCompleted = _nodes.Count <= 0;
}
public string RunId { get; internal set; }
public LevelThemeType ThemeType { get; internal set; }
public int CurrentNodeIndex { get; internal set; }
public bool IsCompleted { get; internal set; }
public BackpackInventoryData RunInventorySnapshot { get; internal set; }
public IReadOnlyList<RunNodeState> Nodes => _nodes;
public int NodeCount => _nodes.Count;
public RunNodeState CurrentNode
{
get
{
if (CurrentNodeIndex < 0 || CurrentNodeIndex >= _nodes.Count)
{
return null;
}
return _nodes[CurrentNodeIndex];
}
}
public bool CanEnterCurrentNode => !IsCompleted && CurrentNode != null && CurrentNode.Status == RunNodeStatus.Available;
public RunNodeState GetNodeBySequenceIndex(int sequenceIndex)
{
if (sequenceIndex < 0)
{
return null;
}
foreach (RunNodeState nodeState in _nodes)
{
if (nodeState.SequenceIndex == sequenceIndex)
{
return nodeState;
}
}
return null;
}
public static bool IsBossNode(RunNodeState nodeState)
{
return nodeState != null && nodeState.NodeType == RunNodeType.BossCombat;
}
public int CompletedNodeCount
{
get
{
int count = 0;
foreach (var nodeState in _nodes)
{
if (nodeState.Status == RunNodeStatus.Completed)
{
count++;
}
}
return count;
}
}
internal void ReplaceInventorySnapshot(BackpackInventoryData inventorySnapshot)
{
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4de7f8a43a9f4be8b0e1a5d6c7f80911
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,56 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class RunStateAdvanceService
{
public static bool TryCompleteCurrentNode(
RunState runState,
RunNodeCompletionStatus completionStatus,
BackpackInventoryData inventorySnapshotAfterNode)
{
if (runState == null || runState.IsCompleted)
{
return false;
}
RunNodeState currentNode = runState.CurrentNode;
if (currentNode == null || currentNode.Status != RunNodeStatus.Available)
{
return false;
}
runState.ReplaceInventorySnapshot(inventorySnapshotAfterNode);
if (completionStatus == RunNodeCompletionStatus.Exception)
{
currentNode.Status = RunNodeStatus.Exception;
return true;
}
if (completionStatus != RunNodeCompletionStatus.Completed)
{
return false;
}
currentNode.Status = RunNodeStatus.Completed;
int nextIndex = runState.CurrentNodeIndex + 1;
if (nextIndex >= runState.Nodes.Count)
{
runState.CurrentNodeIndex = runState.Nodes.Count;
runState.IsCompleted = true;
return true;
}
runState.CurrentNodeIndex = nextIndex;
RunNodeState nextNode = runState.CurrentNode;
if (nextNode != null && nextNode.Status == RunNodeStatus.Locked)
{
nextNode.Status = RunNodeStatus.Available;
}
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f1c5db7a94a44b9aa5f7745ca0c2202
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class RunStateFactory
{
public static RunState CreateFixedRun(
LevelThemeType themeType,
BackpackInventoryData initialInventorySnapshot,
string runId = null)
{
IReadOnlyList<RunNodeSeed> fixedNodeSeeds = FixedRunNodeSequenceBuilder.Build(themeType);
return Create(themeType, initialInventorySnapshot, fixedNodeSeeds, runId);
}
public static RunState Create(
LevelThemeType themeType,
BackpackInventoryData initialInventorySnapshot,
IEnumerable<RunNodeSeed> nodeSeeds,
string runId = null)
{
List<RunNodeState> nodes = new List<RunNodeState>();
if (nodeSeeds != null)
{
int sequenceIndex = 0;
foreach (RunNodeSeed seed in nodeSeeds)
{
if (seed == null)
{
continue;
}
nodes.Add(new RunNodeState
{
NodeId = seed.NodeId > 0 ? seed.NodeId : sequenceIndex + 1,
NodeType = seed.NodeType,
ThemeType = seed.ThemeType == LevelThemeType.None ? themeType : seed.ThemeType,
LinkedLevelId = seed.LinkedLevelId,
SequenceIndex = seed.SequenceIndex >= 0 ? seed.SequenceIndex : sequenceIndex,
Status = sequenceIndex == 0 ? RunNodeStatus.Available : RunNodeStatus.Locked
});
sequenceIndex++;
}
}
return new RunState(runId, themeType, nodes, initialInventorySnapshot);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9b14d5010eb149678d2f4b269d6b8401
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: