From 9c5871b518fa70d41a87e8b31320237e92cabbed Mon Sep 17 00:00:00 2001
From: SepComet <2428390463@qq.com>
Date: Wed, 11 Mar 2026 16:48:26 +0800
Subject: [PATCH] S5-04
---
Assets/GameMain/DataTables/Event.txt | 6 +-
Assets/GameMain/DataTables/TagConfig.txt | 24 ++--
.../GameMain/Scripts/DataTable/DRTagConfig.cs | 2 +-
.../CombatParticipantTowerValidationText.cs | 28 +++++
...mbatParticipantTowerValidationText.cs.meta | 11 ++
.../Procedure/ProcedureMain/ProcedureMain.cs | 31 ++++-
...ocedureMainCombatEntryValidationService.cs | 25 +----
...edureMainParticipantTowerCleanupService.cs | 106 ++++++++++++++++++
...MainParticipantTowerCleanupService.cs.meta | 11 ++
.../UI/Game/Controller/RepoFormController.cs | 8 +-
.../RepoParticipantAssignDialogUtility.cs | 45 ++++++++
...RepoParticipantAssignDialogUtility.cs.meta | 11 ++
.../Scripts/Utility/InventorySeedUtility.cs | 2 +-
.../Scripts/Utility/ItemDescUtility.cs | 66 ++++++++++-
Assets/Tests/EditMode/ItemDescUtilityTests.cs | 92 +++++++++++++++
.../EditMode/ItemDescUtilityTests.cs.meta | 11 ++
...MainParticipantTowerCleanupServiceTests.cs | 85 ++++++++++++++
...articipantTowerCleanupServiceTests.cs.meta | 11 ++
...RepoParticipantAssignDialogUtilityTests.cs | 69 ++++++++++++
...articipantAssignDialogUtilityTests.cs.meta | 11 ++
数据表/convert.py | 3 +-
21 files changed, 611 insertions(+), 47 deletions(-)
create mode 100644 Assets/GameMain/Scripts/Definition/CombatParticipantTowerValidationText.cs
create mode 100644 Assets/GameMain/Scripts/Definition/CombatParticipantTowerValidationText.cs.meta
create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs
create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs.meta
create mode 100644 Assets/GameMain/Scripts/UI/Game/RepoParticipantAssignDialogUtility.cs
create mode 100644 Assets/GameMain/Scripts/UI/Game/RepoParticipantAssignDialogUtility.cs.meta
create mode 100644 Assets/Tests/EditMode/ItemDescUtilityTests.cs
create mode 100644 Assets/Tests/EditMode/ItemDescUtilityTests.cs.meta
create mode 100644 Assets/Tests/EditMode/ProcedureMainParticipantTowerCleanupServiceTests.cs
create mode 100644 Assets/Tests/EditMode/ProcedureMainParticipantTowerCleanupServiceTests.cs.meta
create mode 100644 Assets/Tests/EditMode/RepoParticipantAssignDialogUtilityTests.cs
create mode 100644 Assets/Tests/EditMode/RepoParticipantAssignDialogUtilityTests.cs.meta
diff --git a/Assets/GameMain/DataTables/Event.txt b/Assets/GameMain/DataTables/Event.txt
index c51c58c..b8190fb 100644
--- a/Assets/GameMain/DataTables/Event.txt
+++ b/Assets/GameMain/DataTables/Event.txt
@@ -1,6 +1,6 @@
# Id 列1 Title Description Options
# int string string string
# 事件编号 策划备注 事件题目 事件描述 事件选项
- 1 赌马 一名商人邀请你下注。赢了就能赚一笔。 [{\"optionText\":\"下注 100(金)- 稳健:70% 赢 150\",\"requirements\":[{\"type\":\"GoldAtLeast\",\"param\":{\"Count\":100}}],\"costEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":-100}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":150}}],\"probability\":0.7},{\"optionText\":\"下注 100(金)- 激进:30% 赢 250\",\"requirements\":[{\"type\":\"GoldAtLeast\",\"param\":{\"Count\":100}}],\"costEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":-100}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":250}}],\"probability\":0.3}]
- 2 工匠的熔炉 工匠以金币交换防御塔组件。 [{\"optionText\":\"交出 3 个白色组件,获得 50 金币\",\"requirements\":[{\"type\":\"CompCountAtLeast\",\"param\":{\"Count\":3,\"Rarity\":\"White\"}}],\"costEffects\":[{\"type\":\"RemoveRandomComps\",\"param\":{\"Count\":3,\"Rarity\":\"White\"}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":50}}]},{\"optionText\":\"交出 2 个绿色组件,获得 70 金币\",\"requirements\":[{\"type\":\"CompCountAtLeast\",\"param\":{\"Count\":2,\"Rarity\":\"Green\"}}],\"costEffects\":[{\"type\":\"RemoveRandomComps\",\"param\":{\"Count\":2,\"Rarity\":\"Green\"}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":70}}]},{\"optionText\":\"交出 1 个蓝色组件,获得 80 金币\",\"requirements\":[{\"type\":\"CompCountAtLeast\",\"param\":{\"Count\":1,\"Rarity\":\"Blue\"}}],\"costEffects\":[{\"type\":\"RemoveRandomComps\",\"param\":{\"Count\":1,\"Rarity\":\"Blue\"}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":80}}]},{\"optionText\":\"拒绝\",\"rewardEffects\":[]}]
- 3 代价与回报 某种黑暗力量向你索取代价。 [{\"optionText\":\"展示你的防御塔\",\"requirements\":[{\"type\":\"TowerCountAtLeast\",\"param\":{\"Count\":2}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":50}}]},{\"optionText\":\"离开\",\"rewardEffects\":[]}]
+ 1 赌马 一名商人邀请你下注。赢了就能赚一笔。 [{"optionText":"下注 100(金)- 稳健:70% 赢 150","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":150}}],"probability":0.7},{"optionText":"下注 100(金)- 激进:30% 赢 250","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":250}}],"probability":0.3}]
+ 2 工匠的熔炉 工匠以金币交换防御塔组件。 [{"optionText":"交出 3 个白色组件,获得 50 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":3,"Rarity":"White"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":3,"Rarity":"White"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"交出 2 个绿色组件,获得 70 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"Green"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"Green"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":70}}]},{"optionText":"交出 1 个蓝色组件,获得 80 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":1,"Rarity":"Blue"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":1,"Rarity":"Blue"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":80}}]},{"optionText":"拒绝","rewardEffects":[]}]
+ 3 代价与回报 某种黑暗力量向你索取代价。 [{"optionText":"展示你的防御塔","requirements":[{"type":"TowerCountAtLeast","param":{"Count":2}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"离开","rewardEffects":[]}]
diff --git a/Assets/GameMain/DataTables/TagConfig.txt b/Assets/GameMain/DataTables/TagConfig.txt
index 7defe4f..6f6160a 100644
--- a/Assets/GameMain/DataTables/TagConfig.txt
+++ b/Assets/GameMain/DataTables/TagConfig.txt
@@ -1,15 +1,15 @@
# Id 列1 TagType TriggerPhase Description Param
# int TagType TagTriggerPhase string string
# Tag配置编号 策划备注 所属Tag类型 触发阶段 描述 参数Json
- 1 元素 Fire OnAfterHit 持续对敌人造成火伤害 {\"BurnDurationSeconds\":3,\"BurnDamagePerSecondPerStack\":20,\"MaxEffectiveStack\":5}
- 2 元素 BurnSpread None 燃烧向邻近敌人传播 {\"SpreadRadius\":2,\"SpreadDamageRate\":1}
- 3 元素 IgniteBurst None 燃烧结束或击杀时爆炸 {\"BurstRadius\":1,\"BurstDamageRate\":0.5}
- 4 元素 Inferno OnAfterHit 强化燃烧伤害或持续时间 {\"BonusBurnDurationSeconds\":0.1,\"BonusBurnDamagePerSecondPerStack\":0.1}
- 5 控制 Ice OnAfterHit 命中附加减速 {\"SlowDurationSeconds\":2,\"SlowRatioPerStack\":0.2,\"MinMoveSpeedMultiplier\":0.4}
- 6 控制 FreezeMask None 冻结积累条 / 冻结面具机制 {\"FreezeBuildUpPerStack\":0.1}
- 7 控制 Shatter OnBeforeHit 对已减速 / 已冻结目标增伤 {\"RequiresSlowedTarget\":true,\"DamageBonusPerStack\":0.1}
- 8 控制 AbsoluteZero OnAfterHit 强化减速,或提高冻结触发速度 {\"BonusSlowDurationSeconds\":0.1,\"BonusSlowRatioPerStack\":0.2}
- 9 穿透 Pierce None 子弹贯穿多个目标 {\"ExtraPierceCount\":2}
- 10 穿透 Crit OnBeforeHit 命中前按概率暴击 {\"CritChancePerStack\":0.1,\"CritDamageMultiplier\":1.5}
- 11 穿透 Overpenetrate None 贯穿后保留部分伤害继续飞行 {\"ExtraPenetrationCount\":0.1,\"RemainingDamageRate\":0.2}
- 12 穿透 Execution OnBeforeHit 对低血量目标增伤或直接处决 {\"TargetHealthThreshold\":0.3,\"DamageBonusPerStack\":0.5}
+ 1 元素 Fire OnAfterHit 持续对敌人造成火伤害 {"BurnDurationSeconds":3,"BurnDamagePerSecondPerStack":20,"MaxEffectiveStack":5}
+ 2 元素 BurnSpread None 燃烧向邻近敌人传播 {"SpreadRadius":2,"SpreadDamageRate":1}
+ 3 元素 IgniteBurst None 燃烧结束或击杀时爆炸 {"BurstRadius":1,"BurstDamageRate":0.5}
+ 4 元素 Inferno OnAfterHit 强化燃烧伤害或持续时间 {"BonusBurnDurationSeconds":0.1,"BonusBurnDamagePerSecondPerStack":0.1}
+ 5 控制 Ice OnAfterHit 命中附加减速 {"SlowDurationSeconds":2,"SlowRatioPerStack":0.2,"MinMoveSpeedMultiplier":0.4}
+ 6 控制 FreezeMask None 冻结积累条 / 冻结面具机制 {"FreezeBuildUpPerStack":0.1}
+ 7 控制 Shatter OnBeforeHit 对已减速 / 已冻结目标增伤 {"RequiresSlowedTarget":true,"DamageBonusPerStack":0.1}
+ 8 控制 AbsoluteZero OnAfterHit 强化减速,或提高冻结触发速度 {"BonusSlowDurationSeconds":0.1,"BonusSlowRatioPerStack":0.2}
+ 9 穿透 Pierce None 子弹贯穿多个目标 {"ExtraPierceCount":2}
+ 10 穿透 Crit OnBeforeHit 命中前按概率暴击 {"CritChancePerStack":0.1,"CritDamageMultiplier":1.5}
+ 11 穿透 Overpenetrate None 贯穿后保留部分伤害继续飞行 {"ExtraPenetrationCount":0.1,"RemainingDamageRate":0.2}
+ 12 穿透 Execution OnBeforeHit 对低血量目标增伤或直接处决 {"TargetHealthThreshold":0.3,"DamageBonusPerStack":0.5}
diff --git a/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs b/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs
index b8dde6a..e9000f2 100644
--- a/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs
+++ b/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs
@@ -38,4 +38,4 @@ namespace GeometryTD.DataTable
return true;
}
}
-}
+}
\ No newline at end of file
diff --git a/Assets/GameMain/Scripts/Definition/CombatParticipantTowerValidationText.cs b/Assets/GameMain/Scripts/Definition/CombatParticipantTowerValidationText.cs
new file mode 100644
index 0000000..afe5b8d
--- /dev/null
+++ b/Assets/GameMain/Scripts/Definition/CombatParticipantTowerValidationText.cs
@@ -0,0 +1,28 @@
+namespace GeometryTD.Definition
+{
+ public static class CombatParticipantTowerValidationText
+ {
+ public static string GetFailureReasonMessage(CombatParticipantTowerValidationFailureReason failureReason)
+ {
+ switch (failureReason)
+ {
+ case CombatParticipantTowerValidationFailureReason.TowerMissing:
+ return "已不存在,无法参战。";
+ case CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent:
+ return "缺少枪口组件。";
+ case CombatParticipantTowerValidationFailureReason.MissingBearingComponent:
+ return "缺少轴承组件。";
+ case CombatParticipantTowerValidationFailureReason.MissingBaseComponent:
+ return "缺少底座组件。";
+ case CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent:
+ return "枪口组件耐久为 0,无法参战。";
+ case CombatParticipantTowerValidationFailureReason.BrokenBearingComponent:
+ return "轴承组件耐久为 0,无法参战。";
+ case CombatParticipantTowerValidationFailureReason.BrokenBaseComponent:
+ return "底座组件耐久为 0,无法参战。";
+ default:
+ return "不满足当前参战条件。";
+ }
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Definition/CombatParticipantTowerValidationText.cs.meta b/Assets/GameMain/Scripts/Definition/CombatParticipantTowerValidationText.cs.meta
new file mode 100644
index 0000000..f8f3a6a
--- /dev/null
+++ b/Assets/GameMain/Scripts/Definition/CombatParticipantTowerValidationText.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d7cba7cf0b7a4bfe96f172620db8d30d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs
index 5ed0f84..3b1361e 100644
--- a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs
+++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs
@@ -10,6 +10,8 @@ namespace GeometryTD.Procedure
{
public class ProcedureMain : ProcedureBase
{
+ private const int MaxParticipantTowerCount = 4;
+
public override bool UseNativeDialog => false;
private RepoFormUseCase _repoFormUseCase;
@@ -188,8 +190,23 @@ namespace GeometryTD.Procedure
snapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
}
- HandleRunAdvanceResult(
- ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, snapshot));
+ ProcedureMainParticipantTowerCleanupResult cleanupResult =
+ ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers(
+ snapshot,
+ MaxParticipantTowerCount);
+ if (cleanupResult.HasAnyRemovedTower && GameEntry.PlayerInventory != null)
+ {
+ GameEntry.PlayerInventory.ReplaceInventorySnapshot(snapshot);
+ }
+
+ ProcedureMainRunAdvanceResult advanceResult =
+ ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, snapshot);
+ HandleRunAdvanceResult(advanceResult);
+ if (cleanupResult.HasAnyRemovedTower &&
+ advanceResult != ProcedureMainRunAdvanceResult.RunCompleted)
+ {
+ OpenRemovedParticipantTowerDialog(cleanupResult);
+ }
}
private void OnNodeMapNodeEnterRequested(object sender, GameEventArgs e)
@@ -404,6 +421,14 @@ namespace GeometryTD.Procedure
});
}
+ private void OpenRemovedParticipantTowerDialog(ProcedureMainParticipantTowerCleanupResult cleanupResult)
+ {
+ GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
+ GameEntry.UIRouter.OpenUI(
+ UIFormType.DialogForm,
+ ProcedureMainParticipantTowerCleanupService.BuildRemovedTowerDialogRawData(cleanupResult));
+ }
+
private void OnRunCompleteDialogConfirmed(object userData)
{
_ = userData;
@@ -423,4 +448,4 @@ namespace GeometryTD.Procedure
_isReturnToMenuPending = true;
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs
index 9093b20..c116223 100644
--- a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs
+++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs
@@ -114,33 +114,10 @@ namespace GeometryTD.Procedure
builder.Append("塔 #");
builder.Append(result.TowerInstanceId);
builder.Append(' ');
- builder.Append(GetFailureReasonMessage(result.FailureReason));
+ builder.Append(CombatParticipantTowerValidationText.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 "缺少底座组件。";
- case CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent:
- return "枪口组件耐久为 0,无法参战。";
- case CombatParticipantTowerValidationFailureReason.BrokenBearingComponent:
- return "轴承组件耐久为 0,无法参战。";
- case CombatParticipantTowerValidationFailureReason.BrokenBaseComponent:
- return "底座组件耐久为 0,无法参战。";
- default:
- return "不满足当前参战条件。";
- }
- }
}
}
diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs
new file mode 100644
index 0000000..cf551f9
--- /dev/null
+++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs
@@ -0,0 +1,106 @@
+using System.Collections.Generic;
+using GeometryTD.CustomUtility;
+using GeometryTD.Definition;
+using GeometryTD.UI;
+
+namespace GeometryTD.Procedure
+{
+ public sealed class ProcedureMainParticipantTowerCleanupResult
+ {
+ public List RemovedResults { get; } = new();
+
+ public bool HasAnyRemovedTower => RemovedResults.Count > 0;
+ }
+
+ public static class ProcedureMainParticipantTowerCleanupService
+ {
+ public static ProcedureMainParticipantTowerCleanupResult RemoveBrokenParticipantTowers(
+ BackpackInventoryData inventory,
+ int maxParticipantCount)
+ {
+ ProcedureMainParticipantTowerCleanupResult result = new ProcedureMainParticipantTowerCleanupResult();
+ if (inventory == null)
+ {
+ return result;
+ }
+
+ CombatParticipantTowerValidationSummary summary =
+ CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory);
+ if (summary.InvalidResults == null || summary.InvalidResults.Count <= 0)
+ {
+ return result;
+ }
+
+ HashSet removedTowerIds = new HashSet();
+ for (int i = 0; i < summary.InvalidResults.Count; i++)
+ {
+ CombatParticipantTowerValidationResult invalidResult = summary.InvalidResults[i];
+ if (invalidResult == null ||
+ invalidResult.TowerInstanceId <= 0 ||
+ !IsBrokenFailureReason(invalidResult.FailureReason) ||
+ !removedTowerIds.Add(invalidResult.TowerInstanceId))
+ {
+ continue;
+ }
+
+ if (InventoryParticipantUtility.TryRemoveParticipantTower(
+ inventory,
+ invalidResult.TowerInstanceId,
+ maxParticipantCount))
+ {
+ result.RemovedResults.Add(invalidResult);
+ }
+ }
+
+ return result;
+ }
+
+ public static DialogFormRawData BuildRemovedTowerDialogRawData(
+ ProcedureMainParticipantTowerCleanupResult cleanupResult)
+ {
+ return new DialogFormRawData
+ {
+ Mode = 1,
+ Title = "出战塔已损坏",
+ Message = BuildRemovedTowerDialogMessage(cleanupResult),
+ PauseGame = false,
+ ConfirmText = "知道了"
+ };
+ }
+
+ private static string BuildRemovedTowerDialogMessage(
+ ProcedureMainParticipantTowerCleanupResult cleanupResult)
+ {
+ if (cleanupResult == null || !cleanupResult.HasAnyRemovedTower)
+ {
+ return "当前没有需要移出参战区的损坏防御塔。";
+ }
+
+ System.Text.StringBuilder builder = new System.Text.StringBuilder();
+ builder.Append("以下防御塔已损坏,已自动移出参战区:");
+ for (int i = 0; i < cleanupResult.RemovedResults.Count; i++)
+ {
+ CombatParticipantTowerValidationResult removedResult = cleanupResult.RemovedResults[i];
+ if (removedResult == null)
+ {
+ continue;
+ }
+
+ builder.Append('\n');
+ builder.Append("塔 #");
+ builder.Append(removedResult.TowerInstanceId);
+ builder.Append(' ');
+ builder.Append(CombatParticipantTowerValidationText.GetFailureReasonMessage(removedResult.FailureReason));
+ }
+
+ return builder.ToString();
+ }
+
+ private static bool IsBrokenFailureReason(CombatParticipantTowerValidationFailureReason failureReason)
+ {
+ return failureReason == CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent ||
+ failureReason == CombatParticipantTowerValidationFailureReason.BrokenBearingComponent ||
+ failureReason == CombatParticipantTowerValidationFailureReason.BrokenBaseComponent;
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs.meta
new file mode 100644
index 0000000..4195545
--- /dev/null
+++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 597a543063e14117a16e3530f9cb4949
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.cs b/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.cs
index 3d1441a..e23b3ad 100644
--- a/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.cs
+++ b/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.cs
@@ -185,7 +185,10 @@ namespace GeometryTD.UI
return;
}
- Form.SetRepoItemSelected(args.ItemId, args.Assigned);
+ bool isSelected = _compAreaTowerIds.Contains(args.ItemId)
+ ? _participantTowerIds.Contains(args.ItemId)
+ : args.Assigned;
+ Form.SetRepoItemSelected(args.ItemId, isSelected);
}
private void OnCombineSlotClicked(object sender, GameEventArgs e)
@@ -284,6 +287,9 @@ namespace GeometryTD.UI
result.TowerInstanceId,
result.FailureReason,
result.ValidationFailureReason);
+ GameEntry.UIRouter.OpenUI(
+ UIFormType.DialogForm,
+ RepoParticipantAssignDialogUtility.BuildDialog(result, MaxParticipantCount));
}
return;
diff --git a/Assets/GameMain/Scripts/UI/Game/RepoParticipantAssignDialogUtility.cs b/Assets/GameMain/Scripts/UI/Game/RepoParticipantAssignDialogUtility.cs
new file mode 100644
index 0000000..39da8af
--- /dev/null
+++ b/Assets/GameMain/Scripts/UI/Game/RepoParticipantAssignDialogUtility.cs
@@ -0,0 +1,45 @@
+using GeometryTD.Definition;
+
+namespace GeometryTD.UI
+{
+ public static class RepoParticipantAssignDialogUtility
+ {
+ public static DialogFormRawData BuildDialog(
+ ParticipantTowerAssignResult result,
+ int maxParticipantCount)
+ {
+ return new DialogFormRawData
+ {
+ Mode = 1,
+ Title = "无法加入参战区",
+ Message = BuildMessage(result, maxParticipantCount),
+ PauseGame = false,
+ ConfirmText = "知道了"
+ };
+ }
+
+ private static string BuildMessage(
+ ParticipantTowerAssignResult result,
+ int maxParticipantCount)
+ {
+ if (result == null)
+ {
+ return "当前无法加入参战区,请稍后重试。";
+ }
+
+ switch (result.FailureReason)
+ {
+ case ParticipantTowerAssignFailureReason.TowerMissing:
+ return $"塔 #{result.TowerInstanceId} 已不存在,无法加入参战区。";
+ case ParticipantTowerAssignFailureReason.InvalidTower:
+ return $"塔 #{result.TowerInstanceId} {CombatParticipantTowerValidationText.GetFailureReasonMessage(result.ValidationFailureReason)}";
+ case ParticipantTowerAssignFailureReason.AlreadyAssigned:
+ return $"塔 #{result.TowerInstanceId} 已在参战区中。";
+ case ParticipantTowerAssignFailureReason.ParticipantAreaFull:
+ return $"参战区已满,最多只能放入 {maxParticipantCount} 座塔。";
+ default:
+ return "当前无法加入参战区,请稍后重试。";
+ }
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/UI/Game/RepoParticipantAssignDialogUtility.cs.meta b/Assets/GameMain/Scripts/UI/Game/RepoParticipantAssignDialogUtility.cs.meta
new file mode 100644
index 0000000..e3566b4
--- /dev/null
+++ b/Assets/GameMain/Scripts/UI/Game/RepoParticipantAssignDialogUtility.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6e6500e5312e4f2cb788f03f0aa8ab0c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Utility/InventorySeedUtility.cs b/Assets/GameMain/Scripts/Utility/InventorySeedUtility.cs
index 868b7cf..d6b3a34 100644
--- a/Assets/GameMain/Scripts/Utility/InventorySeedUtility.cs
+++ b/Assets/GameMain/Scripts/Utility/InventorySeedUtility.cs
@@ -117,7 +117,7 @@ namespace GeometryTD.CustomUtility
ConfigId = 3,
Name = "穿透枪口",
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Purple),
- Endurance = 97f,
+ Endurance = 1f,
IsAssembledIntoTower = false,
AttackDamage = new[] { 50, 55, 60, 80, 90 },
DamageRandomRate = 0.02f,
diff --git a/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs b/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs
index c475f05..8031d5b 100644
--- a/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs
+++ b/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs
@@ -70,6 +70,14 @@ namespace GeometryTD.CustomUtility
sb.Append(BuildTowerDesc(tower.Stats ?? new TowerStatsData()));
float enduranceRate = ResolveTowerEnduranceRate(tower, muzzleMap, bearingMap, baseMap);
sb.AppendLine($"平均耐久: {enduranceRate * 100f:0.#}");
+ CombatParticipantTowerValidationFailureReason failureReason =
+ ResolveTowerValidationFailureReason(tower, muzzleMap, bearingMap, baseMap);
+ if (failureReason != CombatParticipantTowerValidationFailureReason.None)
+ {
+ sb.AppendLine("状态:已损坏");
+ sb.Append($"参战限制:{CombatParticipantTowerValidationText.GetFailureReasonMessage(failureReason)}");
+ }
+
return sb.ToString();
}
@@ -139,6 +147,7 @@ namespace GeometryTD.CustomUtility
sb.AppendLine($"攻击方式:{ConvertAttackMethod(muzzleData.AttackMethodType)}");
sb.AppendLine($"当前耐久:{muzzleData.Endurance}");
+ AppendComponentBrokenStatus(sb, muzzleData);
return sb.ToString();
}
@@ -164,6 +173,7 @@ namespace GeometryTD.CustomUtility
sb.Append('\n');
sb.AppendLine($"当前耐久:{bearingData.Endurance}");
+ AppendComponentBrokenStatus(sb, bearingData);
return sb.ToString();
}
@@ -183,10 +193,65 @@ namespace GeometryTD.CustomUtility
sb.AppendLine($"伤害属性:{ConvertAttackProperty(baseData.AttackPropertyType)}");
sb.AppendLine($"当前耐久:{baseData.Endurance}");
+ AppendComponentBrokenStatus(sb, baseData);
return sb.ToString();
}
+ private static void AppendComponentBrokenStatus(StringBuilder sb, TowerCompItemData component)
+ {
+ if (sb == null || component == null || component.Endurance > 0f)
+ {
+ return;
+ }
+
+ sb.AppendLine("状态:已损坏,无法参战。");
+ }
+
+ private static CombatParticipantTowerValidationFailureReason ResolveTowerValidationFailureReason(
+ TowerItemData tower,
+ IReadOnlyDictionary muzzleMap,
+ IReadOnlyDictionary bearingMap,
+ IReadOnlyDictionary baseMap)
+ {
+ if (tower == null)
+ {
+ return CombatParticipantTowerValidationFailureReason.TowerMissing;
+ }
+
+ if (muzzleMap == null || !muzzleMap.TryGetValue(tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzle))
+ {
+ return CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent;
+ }
+
+ if (muzzle.Endurance <= 0f)
+ {
+ return CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent;
+ }
+
+ if (bearingMap == null || !bearingMap.TryGetValue(tower.BearingComponentInstanceId, out BearingCompItemData bearing))
+ {
+ return CombatParticipantTowerValidationFailureReason.MissingBearingComponent;
+ }
+
+ if (bearing.Endurance <= 0f)
+ {
+ return CombatParticipantTowerValidationFailureReason.BrokenBearingComponent;
+ }
+
+ if (baseMap == null || !baseMap.TryGetValue(tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
+ {
+ return CombatParticipantTowerValidationFailureReason.MissingBaseComponent;
+ }
+
+ if (baseComp.Endurance <= 0f)
+ {
+ return CombatParticipantTowerValidationFailureReason.BrokenBaseComponent;
+ }
+
+ return CombatParticipantTowerValidationFailureReason.None;
+ }
+
public static string ConvertAttackMethod(AttackMethodType type)
{
return type switch
@@ -215,4 +280,3 @@ namespace GeometryTD.CustomUtility
}
}
-
diff --git a/Assets/Tests/EditMode/ItemDescUtilityTests.cs b/Assets/Tests/EditMode/ItemDescUtilityTests.cs
new file mode 100644
index 0000000..41ea80e
--- /dev/null
+++ b/Assets/Tests/EditMode/ItemDescUtilityTests.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using GeometryTD.CustomUtility;
+using GeometryTD.Definition;
+using NUnit.Framework;
+
+namespace GeometryTD.Tests.EditMode
+{
+ public sealed class ItemDescUtilityTests
+ {
+ [Test]
+ public void BuildMuzzleDesc_Appends_Broken_Status_When_Endurance_Is_Zero()
+ {
+ string description = ItemDescUtility.BuildMuzzleDesc(new MuzzleCompItemData
+ {
+ AttackDamage = new[] { 10 },
+ DamageRandomRate = 0.1f,
+ AttackMethodType = AttackMethodType.NormalBullet,
+ Endurance = 0f
+ });
+
+ StringAssert.Contains("当前耐久:0", description);
+ StringAssert.Contains("状态:已损坏,无法参战。", description);
+ }
+
+ [Test]
+ public void BuildTowerDesc_Appends_Broken_Status_And_Restriction_When_Component_Is_Broken()
+ {
+ TowerItemData tower = CreateTower();
+ Dictionary muzzleMap = new Dictionary
+ {
+ [10001] = new MuzzleCompItemData { InstanceId = 10001, Endurance = 0f }
+ };
+ Dictionary bearingMap = new Dictionary
+ {
+ [20001] = new BearingCompItemData { InstanceId = 20001, Endurance = 100f }
+ };
+ Dictionary baseMap = new Dictionary
+ {
+ [30001] = new BaseCompItemData { InstanceId = 30001, Endurance = 100f }
+ };
+
+ string description = ItemDescUtility.BuildTowerDesc(tower, muzzleMap, bearingMap, baseMap);
+
+ StringAssert.Contains("状态:已损坏", description);
+ StringAssert.Contains("参战限制:枪口组件耐久为 0,无法参战。", description);
+ }
+
+ [Test]
+ public void BuildTowerDesc_Does_Not_Append_Broken_Status_When_Tower_Is_Valid()
+ {
+ TowerItemData tower = CreateTower();
+ Dictionary muzzleMap = new Dictionary
+ {
+ [10001] = new MuzzleCompItemData { InstanceId = 10001, Endurance = 100f }
+ };
+ Dictionary bearingMap = new Dictionary
+ {
+ [20001] = new BearingCompItemData { InstanceId = 20001, Endurance = 100f }
+ };
+ Dictionary baseMap = new Dictionary
+ {
+ [30001] = new BaseCompItemData { InstanceId = 30001, Endurance = 100f }
+ };
+
+ string description = ItemDescUtility.BuildTowerDesc(tower, muzzleMap, bearingMap, baseMap);
+
+ StringAssert.DoesNotContain("状态:已损坏", description);
+ StringAssert.DoesNotContain("参战限制:", description);
+ }
+
+ private static TowerItemData CreateTower()
+ {
+ return new TowerItemData
+ {
+ InstanceId = 90001,
+ MuzzleComponentInstanceId = 10001,
+ BearingComponentInstanceId = 20001,
+ BaseComponentInstanceId = 30001,
+ Stats = new TowerStatsData
+ {
+ AttackDamage = new[] { 10 },
+ DamageRandomRate = 0.1f,
+ AttackMethodType = AttackMethodType.NormalBullet,
+ RotateSpeed = new[] { 1f },
+ AttackRange = new[] { 2f },
+ AttackSpeed = new[] { 1f },
+ AttackPropertyType = AttackPropertyType.Physics
+ }
+ };
+ }
+ }
+}
diff --git a/Assets/Tests/EditMode/ItemDescUtilityTests.cs.meta b/Assets/Tests/EditMode/ItemDescUtilityTests.cs.meta
new file mode 100644
index 0000000..c2bdfd7
--- /dev/null
+++ b/Assets/Tests/EditMode/ItemDescUtilityTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 128f16fc0aa2493f9ffeb226f76d2661
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/EditMode/ProcedureMainParticipantTowerCleanupServiceTests.cs b/Assets/Tests/EditMode/ProcedureMainParticipantTowerCleanupServiceTests.cs
new file mode 100644
index 0000000..6d91d91
--- /dev/null
+++ b/Assets/Tests/EditMode/ProcedureMainParticipantTowerCleanupServiceTests.cs
@@ -0,0 +1,85 @@
+using GeometryTD.Definition;
+using GeometryTD.Procedure;
+using GeometryTD.UI;
+using NUnit.Framework;
+
+namespace GeometryTD.Tests.EditMode
+{
+ public sealed class ProcedureMainParticipantTowerCleanupServiceTests
+ {
+ [Test]
+ public void RemoveBrokenParticipantTowers_Removes_Only_Broken_Participant_Towers()
+ {
+ BackpackInventoryData inventory = CreateInventory();
+ inventory.MuzzleComponents[0].Endurance = 0f;
+ inventory.ParticipantTowerInstanceIds.Add(90002);
+
+ ProcedureMainParticipantTowerCleanupResult result =
+ ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers(inventory, 4);
+
+ Assert.That(result.HasAnyRemovedTower, Is.True);
+ Assert.That(result.RemovedResults.Count, Is.EqualTo(1));
+ Assert.That(result.RemovedResults[0].TowerInstanceId, Is.EqualTo(90001));
+ CollectionAssert.AreEqual(new long[] { 90002 }, inventory.ParticipantTowerInstanceIds);
+ Assert.That(inventory.Towers[0].IsParticipatingInCombat, Is.False);
+ Assert.That(inventory.Towers[1].IsParticipatingInCombat, Is.True);
+ }
+
+ [Test]
+ public void BuildRemovedTowerDialogRawData_Lists_Removed_Broken_Towers()
+ {
+ ProcedureMainParticipantTowerCleanupResult cleanupResult = new ProcedureMainParticipantTowerCleanupResult();
+ cleanupResult.RemovedResults.Add(new CombatParticipantTowerValidationResult
+ {
+ TowerInstanceId = 90001,
+ FailureReason = CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent
+ });
+ cleanupResult.RemovedResults.Add(new CombatParticipantTowerValidationResult
+ {
+ TowerInstanceId = 90002,
+ FailureReason = CombatParticipantTowerValidationFailureReason.BrokenBaseComponent
+ });
+
+ DialogFormRawData rawData =
+ ProcedureMainParticipantTowerCleanupService.BuildRemovedTowerDialogRawData(cleanupResult);
+
+ Assert.That(rawData.Title, Is.EqualTo("出战塔已损坏"));
+ Assert.That(
+ rawData.Message,
+ Is.EqualTo("以下防御塔已损坏,已自动移出参战区:\n塔 #90001 枪口组件耐久为 0,无法参战。\n塔 #90002 底座组件耐久为 0,无法参战。"));
+ Assert.That(rawData.ConfirmText, Is.EqualTo("知道了"));
+ }
+
+ private static BackpackInventoryData CreateInventory()
+ {
+ BackpackInventoryData inventory = new BackpackInventoryData();
+ AddTower(inventory, 90001, 10001, 20001, 30001);
+ AddTower(inventory, 90002, 10002, 20002, 30002);
+ inventory.ParticipantTowerInstanceIds.Add(90001);
+ inventory.ParticipantTowerInstanceIds.Add(90002);
+ inventory.Towers[0].IsParticipatingInCombat = true;
+ inventory.Towers[1].IsParticipatingInCombat = true;
+ return inventory;
+ }
+
+ private static void AddTower(
+ BackpackInventoryData inventory,
+ long towerId,
+ long muzzleId,
+ long bearingId,
+ long baseId)
+ {
+ inventory.MuzzleComponents.Add(new MuzzleCompItemData { InstanceId = muzzleId, Endurance = 100f });
+ inventory.BearingComponents.Add(new BearingCompItemData { InstanceId = bearingId, Endurance = 100f });
+ inventory.BaseComponents.Add(new BaseCompItemData { InstanceId = baseId, Endurance = 100f });
+ inventory.Towers.Add(new TowerItemData
+ {
+ InstanceId = towerId,
+ MuzzleComponentInstanceId = muzzleId,
+ BearingComponentInstanceId = bearingId,
+ BaseComponentInstanceId = baseId,
+ Stats = new TowerStatsData()
+ });
+ }
+ }
+}
diff --git a/Assets/Tests/EditMode/ProcedureMainParticipantTowerCleanupServiceTests.cs.meta b/Assets/Tests/EditMode/ProcedureMainParticipantTowerCleanupServiceTests.cs.meta
new file mode 100644
index 0000000..4e0c038
--- /dev/null
+++ b/Assets/Tests/EditMode/ProcedureMainParticipantTowerCleanupServiceTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7e9f19d3160e4ba2b966d46feae1e8ee
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/EditMode/RepoParticipantAssignDialogUtilityTests.cs b/Assets/Tests/EditMode/RepoParticipantAssignDialogUtilityTests.cs
new file mode 100644
index 0000000..2ad29c7
--- /dev/null
+++ b/Assets/Tests/EditMode/RepoParticipantAssignDialogUtilityTests.cs
@@ -0,0 +1,69 @@
+using GeometryTD.Definition;
+using GeometryTD.UI;
+using NUnit.Framework;
+
+namespace GeometryTD.Tests.EditMode
+{
+ public sealed class RepoParticipantAssignDialogUtilityTests
+ {
+ [Test]
+ public void BuildDialog_Returns_InvalidTower_Message_For_Broken_Component()
+ {
+ DialogFormRawData rawData = RepoParticipantAssignDialogUtility.BuildDialog(
+ new ParticipantTowerAssignResult
+ {
+ TowerInstanceId = 90001,
+ FailureReason = ParticipantTowerAssignFailureReason.InvalidTower,
+ ValidationFailureReason = CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent
+ },
+ 4);
+
+ Assert.That(rawData.Title, Is.EqualTo("无法加入参战区"));
+ Assert.That(rawData.Message, Is.EqualTo("塔 #90001 枪口组件耐久为 0,无法参战。"));
+ Assert.That(rawData.ConfirmText, Is.EqualTo("知道了"));
+ }
+
+ [Test]
+ public void BuildDialog_Returns_InvalidTower_Message_For_Missing_Component()
+ {
+ DialogFormRawData rawData = RepoParticipantAssignDialogUtility.BuildDialog(
+ new ParticipantTowerAssignResult
+ {
+ TowerInstanceId = 90002,
+ FailureReason = ParticipantTowerAssignFailureReason.InvalidTower,
+ ValidationFailureReason = CombatParticipantTowerValidationFailureReason.MissingBaseComponent
+ },
+ 4);
+
+ Assert.That(rawData.Message, Is.EqualTo("塔 #90002 缺少底座组件。"));
+ }
+
+ [Test]
+ public void BuildDialog_Returns_ParticipantAreaFull_Message()
+ {
+ DialogFormRawData rawData = RepoParticipantAssignDialogUtility.BuildDialog(
+ new ParticipantTowerAssignResult
+ {
+ TowerInstanceId = 90003,
+ FailureReason = ParticipantTowerAssignFailureReason.ParticipantAreaFull
+ },
+ 4);
+
+ Assert.That(rawData.Message, Is.EqualTo("参战区已满,最多只能放入 4 座塔。"));
+ }
+
+ [Test]
+ public void BuildDialog_Returns_AlreadyAssigned_Message()
+ {
+ DialogFormRawData rawData = RepoParticipantAssignDialogUtility.BuildDialog(
+ new ParticipantTowerAssignResult
+ {
+ TowerInstanceId = 90004,
+ FailureReason = ParticipantTowerAssignFailureReason.AlreadyAssigned
+ },
+ 4);
+
+ Assert.That(rawData.Message, Is.EqualTo("塔 #90004 已在参战区中。"));
+ }
+ }
+}
diff --git a/Assets/Tests/EditMode/RepoParticipantAssignDialogUtilityTests.cs.meta b/Assets/Tests/EditMode/RepoParticipantAssignDialogUtilityTests.cs.meta
new file mode 100644
index 0000000..e1aee60
--- /dev/null
+++ b/Assets/Tests/EditMode/RepoParticipantAssignDialogUtilityTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: dcce8297c0fb4a539e4ab8db1a20e165
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/数据表/convert.py b/数据表/convert.py
index c8a034c..05a2903 100644
--- a/数据表/convert.py
+++ b/数据表/convert.py
@@ -60,7 +60,7 @@ def convert_excel_to_txt(folder_path='.'):
# 导出设置:
# 1. sep='\t' : 使用制表符分隔
- # 2. quoting=csv.QUOTE_NONE : 不使用引号包裹字段,也不会把 " 变成 ""
+ # 2. quoting=csv.QUOTE_NONE + quotechar='\x00' : 不使用引号包裹字段,且 " 按原样输出
# 3. escapechar='\\' : 如果单元格内恰好有 Tab 键,会用反斜杠转义,防止数据列错位
df.to_csv(
output_file,
@@ -69,6 +69,7 @@ def convert_excel_to_txt(folder_path='.'):
header=False,
encoding='utf-8',
quoting=csv.QUOTE_NONE,
+ quotechar='\x00',
escapechar='\\'
)