阶段 S3 - 收口出战合法性

This commit is contained in:
SepComet
2026-03-09 19:47:46 +08:00
parent 88641f17b0
commit af20eeecfa
3 changed files with 409 additions and 14 deletions
@@ -1,3 +1,4 @@
using System.Text;
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
@@ -30,6 +31,22 @@ namespace GeometryTD.Procedure
ReturnToMenu = 2
}
public enum ProcedureMainCombatEntryBlockReason
{
None = 0,
InventoryUnavailable = 1,
NoValidParticipantTower = 2
}
public sealed class ProcedureMainCombatEntryValidationResult
{
public bool CanEnterCombat => BlockReason == ProcedureMainCombatEntryBlockReason.None;
public ProcedureMainCombatEntryBlockReason BlockReason { get; set; }
public CombatParticipantTowerValidationSummary ValidationSummary { get; set; }
}
public static class ProcedureMainRunFlowService
{
public static ProcedureMainRunAdvanceResult TryAdvanceRun(
@@ -95,6 +112,140 @@ 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 "不满足当前参战条件。";
}
}
}
public class ProcedureMain : ProcedureBase
{
public override bool UseNativeDialog => false;
@@ -314,9 +465,13 @@ namespace GeometryTD.Procedure
{
case RunNodeType.Combat:
case RunNodeType.BossCombat:
if (!HasAvailableParticipantTower())
ProcedureMainCombatEntryValidationResult validationResult =
ProcedureMainCombatEntryValidationService.Validate(
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null);
if (!validationResult.CanEnterCombat)
{
Log.Warning("ProcedureMain blocked combat start. No participant tower is available.");
LogCombatEntryBlocked(currentNode, validationResult);
OpenBlockedCombatDialog(validationResult);
return;
}
@@ -348,6 +503,51 @@ namespace GeometryTD.Procedure
}
}
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)
@@ -375,12 +575,6 @@ namespace GeometryTD.Procedure
}
}
private static bool HasAvailableParticipantTower()
{
return GameEntry.PlayerInventory != null &&
GameEntry.PlayerInventory.GetParticipantTowerSnapshot().Count > 0;
}
private void EnterHubFlow()
{
_flowPhase = ProcedureMainFlowPhase.Hub;
@@ -1,5 +1,6 @@
using GeometryTD.Definition;
using GeometryTD.Procedure;
using GeometryTD.UI;
using NUnit.Framework;
namespace GeometryTD.Tests.EditMode
@@ -170,6 +171,171 @@ namespace GeometryTD.Tests.EditMode
Assert.That(pendingResult, Is.EqualTo(ProcedureMainRunCompletionResult.NoChange));
}
[Test]
public void ValidateCombatEntry_Returns_InventoryUnavailable_When_Inventory_Is_Null()
{
ProcedureMainCombatEntryValidationResult result =
ProcedureMainCombatEntryValidationService.Validate(null);
Assert.That(result.CanEnterCombat, Is.False);
Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.InventoryUnavailable));
Assert.That(result.ValidationSummary, Is.Not.Null);
Assert.That(result.ValidationSummary.HasAnyValidParticipantTower, Is.False);
}
[Test]
public void ValidateCombatEntry_Returns_NoValidParticipantTower_When_ParticipantArea_Is_Empty()
{
BackpackInventoryData inventory = CreateCombatInventory();
inventory.ParticipantTowerInstanceIds.Clear();
ProcedureMainCombatEntryValidationResult result =
ProcedureMainCombatEntryValidationService.Validate(inventory);
Assert.That(result.CanEnterCombat, Is.False);
Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.NoValidParticipantTower));
Assert.That(result.ValidationSummary.HasAnyValidParticipantTower, Is.False);
Assert.That(result.ValidationSummary.ValidTowers.Count, Is.EqualTo(0));
Assert.That(result.ValidationSummary.InvalidResults.Count, Is.EqualTo(0));
}
[Test]
public void ValidateCombatEntry_Returns_NoValidParticipantTower_When_AllParticipantTowers_Are_Invalid()
{
BackpackInventoryData inventory = CreateCombatInventory();
inventory.Towers[0].BaseComponentInstanceId = 99903;
ProcedureMainCombatEntryValidationResult result =
ProcedureMainCombatEntryValidationService.Validate(inventory);
Assert.That(result.CanEnterCombat, Is.False);
Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.NoValidParticipantTower));
Assert.That(result.ValidationSummary.ValidTowers.Count, Is.EqualTo(0));
Assert.That(result.ValidationSummary.InvalidResults.Count, Is.EqualTo(1));
Assert.That(
result.ValidationSummary.InvalidResults[0].FailureReason,
Is.EqualTo(CombatParticipantTowerValidationFailureReason.MissingBaseComponent));
}
[Test]
public void ValidateCombatEntry_Returns_CanEnter_When_At_Least_One_ParticipantTower_Is_Valid()
{
BackpackInventoryData inventory = CreateCombatInventory();
inventory.Towers.Add(new TowerItemData
{
InstanceId = 90002,
Name = "非法塔",
MuzzleComponentInstanceId = 10001,
BearingComponentInstanceId = 20001,
BaseComponentInstanceId = 99903
});
inventory.ParticipantTowerInstanceIds.Add(90002);
ProcedureMainCombatEntryValidationResult result =
ProcedureMainCombatEntryValidationService.Validate(inventory);
Assert.That(result.CanEnterCombat, Is.True);
Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.None));
Assert.That(result.ValidationSummary.ValidTowers.Count, Is.EqualTo(1));
Assert.That(result.ValidationSummary.ValidTowers[0].InstanceId, Is.EqualTo(90001));
Assert.That(result.ValidationSummary.InvalidResults.Count, Is.EqualTo(1));
}
[Test]
public void BuildInvalidParticipantTowerLog_Returns_None_When_InvalidResults_Are_Empty()
{
string log = ProcedureMainCombatEntryValidationService.BuildInvalidParticipantTowerLog(
new CombatParticipantTowerValidationSummary());
Assert.That(log, Is.EqualTo("none"));
}
[Test]
public void BuildInvalidParticipantTowerLog_Formats_TowerIds_And_Reasons()
{
CombatParticipantTowerValidationSummary summary = new CombatParticipantTowerValidationSummary
{
InvalidResults = new[]
{
new CombatParticipantTowerValidationResult
{
TowerInstanceId = 90002,
FailureReason = CombatParticipantTowerValidationFailureReason.MissingBaseComponent
},
new CombatParticipantTowerValidationResult
{
TowerInstanceId = 90003,
FailureReason = CombatParticipantTowerValidationFailureReason.MissingBearingComponent
}
}
};
string log = ProcedureMainCombatEntryValidationService.BuildInvalidParticipantTowerLog(summary);
Assert.That(log, Is.EqualTo("#90002:MissingBaseComponent, #90003:MissingBearingComponent"));
}
[Test]
public void BuildBlockedCombatDialogRawData_Returns_InventoryUnavailable_Message()
{
DialogFormRawData rawData = ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData(
new ProcedureMainCombatEntryValidationResult
{
BlockReason = ProcedureMainCombatEntryBlockReason.InventoryUnavailable,
ValidationSummary = new CombatParticipantTowerValidationSummary()
});
Assert.That(rawData.Title, Is.EqualTo("无法进入战斗"));
Assert.That(rawData.Message, Is.EqualTo("当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。"));
Assert.That(rawData.ConfirmText, Is.EqualTo("知道了"));
Assert.That(rawData.Mode, Is.EqualTo(1));
}
[Test]
public void BuildBlockedCombatDialogRawData_Returns_Generic_Message_When_NoParticipantTower_Is_Assigned()
{
DialogFormRawData rawData = ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData(
new ProcedureMainCombatEntryValidationResult
{
BlockReason = ProcedureMainCombatEntryBlockReason.NoValidParticipantTower,
ValidationSummary = new CombatParticipantTowerValidationSummary()
});
Assert.That(
rawData.Message,
Is.EqualTo("参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。"));
}
[Test]
public void BuildBlockedCombatDialogRawData_Lists_Invalid_Tower_Reasons()
{
DialogFormRawData rawData = ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData(
new ProcedureMainCombatEntryValidationResult
{
BlockReason = ProcedureMainCombatEntryBlockReason.NoValidParticipantTower,
ValidationSummary = new CombatParticipantTowerValidationSummary
{
InvalidResults = new[]
{
new CombatParticipantTowerValidationResult
{
TowerInstanceId = 90002,
FailureReason = CombatParticipantTowerValidationFailureReason.MissingBaseComponent
},
new CombatParticipantTowerValidationResult
{
TowerInstanceId = 90003,
FailureReason = CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent
}
}
}
});
Assert.That(
rawData.Message,
Is.EqualTo("参战区没有可出战的完整塔。\n塔 #90002 缺少底座组件。\n塔 #90003 缺少枪口组件。"));
}
private static RunState CreateTwoNodeRun()
{
return RunStateFactory.Create(
@@ -181,5 +347,38 @@ namespace GeometryTD.Tests.EditMode
new RunNodeSeed { NodeId = 102, NodeType = RunNodeType.Event }
});
}
private static BackpackInventoryData CreateCombatInventory()
{
BackpackInventoryData inventory = new BackpackInventoryData();
inventory.MuzzleComponents.Add(new MuzzleCompItemData
{
InstanceId = 10001,
Name = "枪口",
Endurance = 100f
});
inventory.BearingComponents.Add(new BearingCompItemData
{
InstanceId = 20001,
Name = "轴承",
Endurance = 100f
});
inventory.BaseComponents.Add(new BaseCompItemData
{
InstanceId = 30001,
Name = "底座",
Endurance = 100f
});
inventory.Towers.Add(new TowerItemData
{
InstanceId = 90001,
Name = "合法塔",
MuzzleComponentInstanceId = 10001,
BearingComponentInstanceId = 20001,
BaseComponentInstanceId = 30001
});
inventory.ParticipantTowerInstanceIds.Add(90001);
return inventory;
}
}
}