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='\\' )