From b1b68ebde5a15f43ef565974f4a12764bea631bf Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Wed, 11 Mar 2026 11:20:16 +0800 Subject: [PATCH] S4-07 process 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当前 `Tag.txt -> DRTag -> TagGenerationRuleRegistry` 已形成组件 Tag 生成规则闭环: - `InventoryTagRuleService` 不再主要依赖内部 `MinRarity` 硬编码,而是统一消费 `DRTag` 提供的 `MinRarity + Weight` - `ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 也已切回这一统一入口。 - 现阶段的职责边界明确为:`Tag.txt` 负责基础字典与生成规则,`TagConfig.txt` 负责触发阶段、描述与效果参数。 --- Assets/GameMain/DataTables/LevelPhase.txt | 6 +- Assets/GameMain/DataTables/Tag.txt | 30 +-- Assets/GameMain/DataTables/TagConfig.txt | 66 ++--- .../EnemyDrop/EnemyDropResolver.cs | 57 +--- Assets/GameMain/Scripts/DataTable/DRTag.cs | 9 +- .../ProcedureMainCombatEntryBlockReason.cs | 9 + ...rocedureMainCombatEntryBlockReason.cs.meta | 3 + .../Definition/Enum/ProcedureMainFlowPhase.cs | 9 + .../Enum/ProcedureMainFlowPhase.cs.meta | 3 + .../Enum/ProcedureMainRunAdvanceResult.cs | 10 + .../ProcedureMainRunAdvanceResult.cs.meta | 3 + .../Enum/ProcedureMainRunCompletionResult.cs | 9 + .../ProcedureMainRunCompletionResult.cs.meta | 3 + .../Definition/InventoryTagRuleService.cs | 82 +++--- .../Definition/TagGenerationRuleData.cs | 9 + .../Definition/TagGenerationRuleData.cs.meta | 11 + .../Definition/TagGenerationRuleRegistry.cs | 74 ++++++ .../TagGenerationRuleRegistry.cs.meta | 11 + .../Procedure/Base/ProcedurePreload.cs | 9 + .../Scripts/Procedure/ProcedureMain.meta | 8 + .../FixedRunNodeSequenceBuilder.cs | 0 .../FixedRunNodeSequenceBuilder.cs.meta | 0 .../{ => ProcedureMain}/ProcedureMain.cs | 244 +----------------- .../{ => ProcedureMain}/ProcedureMain.cs.meta | 0 ...rocedureMainCombatEntryValidationResult.cs | 13 + ...ureMainCombatEntryValidationResult.cs.meta | 3 + ...ocedureMainCombatEntryValidationService.cs | 140 ++++++++++ ...reMainCombatEntryValidationService.cs.meta | 3 + .../ProcedureMainNodeEventGuardService.cs | 22 ++ ...ProcedureMainNodeEventGuardService.cs.meta | 3 + .../ProcedureMainRunCompletionService.cs | 26 ++ .../ProcedureMainRunCompletionService.cs.meta | 3 + .../ProcedureMainRunFlowService.cs | 27 ++ .../ProcedureMainRunFlowService.cs.meta | 3 + .../Procedure/{ => ProcedureMain}/RunModel.cs | 0 .../{ => ProcedureMain}/RunModel.cs.meta | 0 .../RunStateAdvanceService.cs | 0 .../RunStateAdvanceService.cs.meta | 0 .../{ => ProcedureMain}/RunStateFactory.cs | 0 .../RunStateFactory.cs.meta | 0 .../UI/Shop/UseCase/ShopFormUseCase.cs | 41 +-- .../EditMode/InventoryTagRuleServiceTests.cs | 100 ++++++- docs/CodeX-TODO.md | 11 +- 数据表/Tag.xlsx | Bin 14576 -> 14927 bytes 数据表/TowerComp.xlsx | Bin 15453 -> 15733 bytes 数据表/convert.py | 7 +- 46 files changed, 615 insertions(+), 452 deletions(-) create mode 100644 Assets/GameMain/Scripts/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs create mode 100644 Assets/GameMain/Scripts/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Enum/ProcedureMainFlowPhase.cs create mode 100644 Assets/GameMain/Scripts/Definition/Enum/ProcedureMainFlowPhase.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunAdvanceResult.cs create mode 100644 Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunAdvanceResult.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunCompletionResult.cs create mode 100644 Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunCompletionResult.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/TagGenerationRuleData.cs create mode 100644 Assets/GameMain/Scripts/Definition/TagGenerationRuleData.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/TagGenerationRuleRegistry.cs create mode 100644 Assets/GameMain/Scripts/Definition/TagGenerationRuleRegistry.cs.meta create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain.meta rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/FixedRunNodeSequenceBuilder.cs (100%) rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/FixedRunNodeSequenceBuilder.cs.meta (100%) rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/ProcedureMain.cs (66%) rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/ProcedureMain.cs.meta (100%) create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs.meta create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs.meta create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs.meta create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs.meta create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs create mode 100644 Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs.meta rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/RunModel.cs (100%) rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/RunModel.cs.meta (100%) rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/RunStateAdvanceService.cs (100%) rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/RunStateAdvanceService.cs.meta (100%) rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/RunStateFactory.cs (100%) rename Assets/GameMain/Scripts/Procedure/{ => ProcedureMain}/RunStateFactory.cs.meta (100%) diff --git a/Assets/GameMain/DataTables/LevelPhase.txt b/Assets/GameMain/DataTables/LevelPhase.txt index b3fa983..551f061 100644 --- a/Assets/GameMain/DataTables/LevelPhase.txt +++ b/Assets/GameMain/DataTables/LevelPhase.txt @@ -5,17 +5,17 @@ 1002 平原1.2 55 TimeElapsed 60 1003 平原1.3 55 TimeElapsed 60 1004 平原1.4 55 EnemiesCleared - 1005 平原1.* 0 + 1005 平原1.* 0 None 2001 平原2.1 55 TimeElapsed 60 2002 平原2.2 55 TimeElapsed 60 2003 平原2.3 55 TimeElapsed 60 2004 平原2.4 55 EnemiesCleared - 2005 平原2.* 0 + 2005 平原2.* 0 None 3001 平原3.1 55 TimeElapsed 60 3002 平原3.2 55 TimeElapsed 60 3003 平原3.3 55 TimeElapsed 60 3004 平原3.4 55 EnemiesCleared - 3005 平原3.* 0 + 3005 平原3.* 0 None 4001 平原4.1 55 TimeElapsed 60 4002 平原4.2 55 TimeElapsed 60 4003 平原4.3 55 TimeElapsed 60 diff --git a/Assets/GameMain/DataTables/Tag.txt b/Assets/GameMain/DataTables/Tag.txt index f07dbaa..5572a8f 100644 --- a/Assets/GameMain/DataTables/Tag.txt +++ b/Assets/GameMain/DataTables/Tag.txt @@ -1,15 +1,15 @@ -# Id 列1 Name MinRarity -# int string RarityType -# Tag编号 策划备注 Tag名 获取最低稀有度 - 1 元素 Fire White - 2 元素 BurnSpread White - 3 元素 IgniteBurst Green - 4 元素 Inferno Purple - 5 控制 Ice White - 6 控制 FreezeMask White - 7 控制 Shatter Green - 8 控制 AbsoluteZero Purple - 9 穿透 Pierce White - 10 穿透 Crit White - 11 穿透 Overpenetrate Green - 12 穿透 Execution Purple +# Id 列1 Name MinRarity Weight +# int string RarityType int +# Tag编号 策划备注 Tag名 获取最低稀有度 权重 + 1 元素 Fire White 20 + 2 元素 BurnSpread White 20 + 3 元素 IgniteBurst Green 15 + 4 元素 Inferno Purple 5 + 5 控制 Ice White 1 + 6 控制 FreezeMask White 20 + 7 控制 Shatter Green 15 + 8 控制 AbsoluteZero Purple 1 + 9 穿透 Pierce White 20 + 10 穿透 Crit White 20 + 11 穿透 Overpenetrate Green 15 + 12 穿透 Execution Purple 5 diff --git a/Assets/GameMain/DataTables/TagConfig.txt b/Assets/GameMain/DataTables/TagConfig.txt index 99c6a28..6f6160a 100644 --- a/Assets/GameMain/DataTables/TagConfig.txt +++ b/Assets/GameMain/DataTables/TagConfig.txt @@ -1,51 +1,15 @@ -# Id 列1 TagType Weight TriggerPhase Description Param -# int TagType int TagTriggerPhase string string -# Tag配置编号 策划备注 所属Tag类型 权重 触发阶段 描述 参数Json - 1 元素 Fire 20 OnAfterHit 持续对敌人造成火伤害 {\ - "BurnDurationSeconds": 3,\ - "BurnDamagePerSecondPerStack": 20,\ - "MaxEffectiveStack": 5\ - } - 2 元素 BurnSpread 20 燃烧向邻近敌人传播 {\ - "SpreadRadius": 0,\ - "SpreadDamageRate": 0\ - } - 3 元素 IgniteBurst 15 燃烧结束或击杀时爆炸 {\ - "BurstRadius": 0,\ - "BurstDamageRate": 0\ - } - 4 元素 Inferno 5 OnAfterHit 强化燃烧伤害或持续时间 {\ - "BonusBurnDurationSeconds": 0,\ - "BonusBurnDamagePerSecondPerStack": 0\ - } - 5 控制 Ice 20 OnAfterHit 命中附加减速 {\ - "SlowDurationSeconds": 2,\ - "SlowRatioPerStack": 0.2,\ - "MinMoveSpeedMultiplier": 0.4\ - } - 6 控制 FreezeMask 20 冻结积累条 / 冻结面具机制 {\ - "FreezeBuildUpPerStack": 0\ - } - 7 控制 Shatter 15 OnBeforeHit 对已减速 / 已冻结目标增伤 {\ - "RequiresSlowedTarget": true,\ - "DamageBonusPerStack": 0\ - } - 8 控制 AbsoluteZero 5 OnAfterHit 强化减速,或提高冻结触发速度 {\ - "BonusSlowDurationSeconds": 1,\ - "BonusSlowRatioPerStack": 0.1\ - } - 9 穿透 Pierce 20 子弹贯穿多个目标 {\ - "ExtraPierceCount": 2\ - } - 10 穿透 Crit 20 OnBeforeHit 命中前按概率暴击 {\ - "CritChancePerStack": 0.1,\ - "CritDamageMultiplier": 1.5\ - } - 11 穿透 Overpenetrate 15 贯穿后保留部分伤害继续飞行 {\ - "ExtraPenetrationCount": 0,\ - "RemainingDamageRate": 0\ - } - 12 穿透 Execution 5 OnBeforeHit 对低血量目标增伤或直接处决 {\ - "TargetHealthThreshold": 0.3,\ - "DamageBonusPerStack": 0.5\ - } +# 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} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs index 5795946..c79736d 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs @@ -17,20 +17,16 @@ namespace GeometryTD.CustomComponent private readonly List _eligibleDropPoolBuffer = new(); private readonly Dictionary _rarityRollWeightBuffer = new(); - private readonly Dictionary _tagMinRarityByTag = new(); - private IDataTable _drOutGameDropPool; private IDataTable _drMuzzleComp; private IDataTable _drBearingComp; private IDataTable _drBaseComp; - private IDataTable _drTag; private long _nextDropItemInstanceId = 1; public void Reset() { _eligibleDropPoolBuffer.Clear(); _rarityRollWeightBuffer.Clear(); - _tagMinRarityByTag.Clear(); _nextDropItemInstanceId = 1; } @@ -307,35 +303,6 @@ namespace GeometryTD.CustomComponent return _drOutGameDropPool; } - private bool EnsureTagMinRarityLookup() - { - if (_tagMinRarityByTag.Count > 0) - { - return true; - } - - _drTag ??= GameEntry.DataTable.GetDataTable(); - if (_drTag == null) - { - return false; - } - - DRTag[] rows = _drTag.GetAllDataRows(); - for (int i = 0; i < rows.Length; i++) - { - DRTag row = rows[i]; - if (row == null || row.Id <= 0) - { - continue; - } - - TagType tagType = (TagType)row.Id; - _tagMinRarityByTag[tagType] = row.MinRarity; - } - - return _tagMinRarityByTag.Count > 0; - } - private bool TryBuildDropItem(DROutGameDropPool row, out TowerCompItemData droppedItem) { droppedItem = null; @@ -372,11 +339,6 @@ namespace GeometryTD.CustomComponent return false; } - if (!EnsureTagMinRarityLookup()) - { - return false; - } - DRMuzzleComp config = _drMuzzleComp.GetDataRow(row.ItemId); if (config == null) { @@ -398,8 +360,7 @@ namespace GeometryTD.CustomComponent rarity, InventoryTagSourceType.Drop, instanceId, - config.Id, - _tagMinRarityByTag), + config.Id), AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty(), DamageRandomRate = config.DamageRandomRate, AttackMethodType = config.AttackMethodType @@ -416,11 +377,6 @@ namespace GeometryTD.CustomComponent return false; } - if (!EnsureTagMinRarityLookup()) - { - return false; - } - DRBearingComp config = _drBearingComp.GetDataRow(row.ItemId); if (config == null) { @@ -442,8 +398,7 @@ namespace GeometryTD.CustomComponent rarity, InventoryTagSourceType.Drop, instanceId, - config.Id, - _tagMinRarityByTag), + config.Id), RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty(), AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty() }; @@ -459,11 +414,6 @@ namespace GeometryTD.CustomComponent return false; } - if (!EnsureTagMinRarityLookup()) - { - return false; - } - DRBaseComp config = _drBaseComp.GetDataRow(row.ItemId); if (config == null) { @@ -485,8 +435,7 @@ namespace GeometryTD.CustomComponent rarity, InventoryTagSourceType.Drop, instanceId, - config.Id, - _tagMinRarityByTag), + config.Id), AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty(), AttackPropertyType = config.AttackPropertyType }; diff --git a/Assets/GameMain/Scripts/DataTable/DRTag.cs b/Assets/GameMain/Scripts/DataTable/DRTag.cs index 054387b..644bb6f 100644 --- a/Assets/GameMain/Scripts/DataTable/DRTag.cs +++ b/Assets/GameMain/Scripts/DataTable/DRTag.cs @@ -6,7 +6,7 @@ using UnityGameFramework.Runtime; namespace GeometryTD.DataTable { /// - /// 枪口组件表 + /// Tag 表 /// public class DRTag : DataRowBase { @@ -22,8 +22,12 @@ namespace GeometryTD.DataTable /// public string Name { get; private set; } + public TagType TagType => (TagType)m_Id; + public RarityType MinRarity { get; private set; } + public int Weight { get; private set; } + public override bool ParseDataRow(string dataRowString, object userData) { string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators); @@ -38,8 +42,9 @@ namespace GeometryTD.DataTable index++; Name = columnStrings[index++]; MinRarity = EnumUtility.Get(columnStrings[index++]); + Weight = int.Parse(columnStrings[index++]); return true; } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs new file mode 100644 index 0000000..a3feef7 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainCombatEntryBlockReason + { + None = 0, + InventoryUnavailable = 1, + NoValidParticipantTower = 2 + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs.meta b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs.meta new file mode 100644 index 0000000..d03926a --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f6d8af9f1d7d4041bfcee904724303e9 +timeCreated: 1773198097 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainFlowPhase.cs b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainFlowPhase.cs new file mode 100644 index 0000000..d9305ba --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainFlowPhase.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainFlowPhase + { + Hub = 0, + NodeActive = 1, + RunCompletedPendingFinish = 2 + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainFlowPhase.cs.meta b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainFlowPhase.cs.meta new file mode 100644 index 0000000..fbe8201 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainFlowPhase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 01450486bd824f47bcd74dda8a160ba4 +timeCreated: 1773198057 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunAdvanceResult.cs b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunAdvanceResult.cs new file mode 100644 index 0000000..6fe78cc --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunAdvanceResult.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainRunAdvanceResult + { + NoChange = 0, + NodeException = 1, + AdvancedToNextNode = 2, + RunCompleted = 3 + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunAdvanceResult.cs.meta b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunAdvanceResult.cs.meta new file mode 100644 index 0000000..d603b9e --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunAdvanceResult.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e9e0fa56d7e84874ac0f9d4a400fda13 +timeCreated: 1773198071 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunCompletionResult.cs b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunCompletionResult.cs new file mode 100644 index 0000000..f5fe2f6 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunCompletionResult.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainRunCompletionResult + { + NoChange = 0, + ShowCompletionDialog = 1, + ReturnToMenu = 2 + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunCompletionResult.cs.meta b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunCompletionResult.cs.meta new file mode 100644 index 0000000..360e4bf --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/ProcedureMainRunCompletionResult.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e3d0df05f77043a99c463a790b5ac35f +timeCreated: 1773198084 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/InventoryTagRuleService.cs b/Assets/GameMain/Scripts/Definition/InventoryTagRuleService.cs index 8dc4bba..4b27ee7 100644 --- a/Assets/GameMain/Scripts/Definition/InventoryTagRuleService.cs +++ b/Assets/GameMain/Scripts/Definition/InventoryTagRuleService.cs @@ -1,33 +1,22 @@ using System; using System.Collections.Generic; +using UnityEngine; +using Random = System.Random; namespace GeometryTD.Definition { public static class InventoryTagRuleService { - private static readonly IReadOnlyDictionary DefaultMinRarityByTag = - new Dictionary - { - { TagType.Fire, RarityType.White }, - { TagType.Ice, RarityType.White }, - { TagType.Crit, RarityType.White }, - { TagType.Shatter, RarityType.Green }, - { TagType.Inferno, RarityType.Purple }, - { TagType.AbsoluteZero, RarityType.Purple }, - { TagType.Execution, RarityType.Purple }, - }; - - private static readonly HashSet SupportedLaunchTags = new HashSet(DefaultMinRarityByTag.Keys); - public static TagType[] ResolveComponentTags( IReadOnlyList possibleTags, RarityType rarity, InventoryTagSourceType sourceType, long itemInstanceId, int configId, - IReadOnlyDictionary minRarityByTag = null) + IReadOnlyDictionary rulesByTag = null) { - TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, minRarityByTag); + IReadOnlyDictionary ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules; + TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, ruleLookup); if (eligibleTags.Length <= 0) { return Array.Empty(); @@ -45,7 +34,7 @@ namespace GeometryTD.Definition TagType[] result = new TagType[finalCount]; for (int i = 0; i < finalCount; i++) { - int index = random.Next(0, pool.Count); + int index = RollWeightedIndex(pool, ruleLookup, random); result[i] = pool[index]; pool.RemoveAt(index); } @@ -56,7 +45,7 @@ namespace GeometryTD.Definition public static TagType[] GetEligibleTags( IReadOnlyList possibleTags, RarityType rarity, - IReadOnlyDictionary minRarityByTag = null) + IReadOnlyDictionary rulesByTag = null) { if (possibleTags == null || possibleTags.Count <= 0) { @@ -64,23 +53,23 @@ namespace GeometryTD.Definition } RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); - IReadOnlyDictionary rarityLookup = minRarityByTag ?? DefaultMinRarityByTag; + IReadOnlyDictionary ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules; HashSet uniqueTags = new HashSet(); for (int i = 0; i < possibleTags.Count; i++) { TagType tagType = possibleTags[i]; - if (!IsSupportedLaunchTag(tagType) || tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType)) + if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType) || !IsSupportedLaunchTag(tagType)) { continue; } - if (!TryGetMinRarity(tagType, rarityLookup, out RarityType minRarity)) + if (!TryGetRule(tagType, ruleLookup, out TagGenerationRuleData rule)) { continue; } - if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(minRarity)) + if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(rule.MinRarity)) { continue; } @@ -123,28 +112,55 @@ namespace GeometryTD.Definition private static bool IsSupportedLaunchTag(TagType tagType) { - return SupportedLaunchTags.Contains(tagType); + return TagConfigRegistry.TryGetDefinition(tagType, out TagDefinitionData definition) && + definition.Config != null && + definition.Config.IsImplemented; } - private static bool TryGetMinRarity( + private static bool TryGetRule( TagType tagType, - IReadOnlyDictionary rarityLookup, - out RarityType minRarity) + IReadOnlyDictionary ruleLookup, + out TagGenerationRuleData rule) { - if (rarityLookup != null && rarityLookup.TryGetValue(tagType, out minRarity)) + if (ruleLookup != null && ruleLookup.TryGetValue(tagType, out rule)) { return true; } - if (DefaultMinRarityByTag.TryGetValue(tagType, out minRarity)) - { - return true; - } - - minRarity = RarityType.White; + rule = null; return false; } + private static int RollWeightedIndex( + IReadOnlyList pool, + IReadOnlyDictionary ruleLookup, + Random random) + { + int totalWeight = 0; + for (int i = 0; i < pool.Count; i++) + { + TagGenerationRuleData rule = ruleLookup[pool[i]]; + Debug.Assert(rule.Weight > 0); + totalWeight += rule.Weight; + } + + Debug.Assert(totalWeight > 0); + + int roll = random.Next(1, totalWeight + 1); + int cumulative = 0; + for (int i = 0; i < pool.Count; i++) + { + TagGenerationRuleData rule = ruleLookup[pool[i]]; + cumulative += rule.Weight; + if (roll <= cumulative) + { + return i; + } + } + + return pool.Count - 1; + } + private static int BuildStableSeed( RarityType rarity, InventoryTagSourceType sourceType, diff --git a/Assets/GameMain/Scripts/Definition/TagGenerationRuleData.cs b/Assets/GameMain/Scripts/Definition/TagGenerationRuleData.cs new file mode 100644 index 0000000..3ec22f6 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/TagGenerationRuleData.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public sealed class TagGenerationRuleData + { + public TagType TagType { get; set; } + public RarityType MinRarity { get; set; } + public int Weight { get; set; } + } +} diff --git a/Assets/GameMain/Scripts/Definition/TagGenerationRuleData.cs.meta b/Assets/GameMain/Scripts/Definition/TagGenerationRuleData.cs.meta new file mode 100644 index 0000000..9782d13 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/TagGenerationRuleData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cfe508e818ce47649a112ecdf86cf26a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/TagGenerationRuleRegistry.cs b/Assets/GameMain/Scripts/Definition/TagGenerationRuleRegistry.cs new file mode 100644 index 0000000..5b5d8e3 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/TagGenerationRuleRegistry.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class TagGenerationRuleRegistry + { + private static readonly Dictionary RulesByTag = CreateDefaultRules(); + + public static IReadOnlyDictionary Rules => RulesByTag; + + public static void ResetToDefaults() + { + RulesByTag.Clear(); + foreach (KeyValuePair pair in CreateDefaultRules()) + { + RulesByTag.Add(pair.Key, pair.Value); + } + } + + public static void LoadFromRows(IEnumerable rows) + { + ResetToDefaults(); + foreach (DRTag row in rows) + { + ApplyRow(row); + } + } + + public static bool TryGetRule(TagType tagType, out TagGenerationRuleData rule) + { + return RulesByTag.TryGetValue(tagType, out rule); + } + + private static Dictionary CreateDefaultRules() + { + return new Dictionary + { + [TagType.Fire] = CreateRule(TagType.Fire, RarityType.White, 20), + [TagType.BurnSpread] = CreateRule(TagType.BurnSpread, RarityType.White, 20), + [TagType.IgniteBurst] = CreateRule(TagType.IgniteBurst, RarityType.Green, 15), + [TagType.Inferno] = CreateRule(TagType.Inferno, RarityType.Purple, 5), + [TagType.Ice] = CreateRule(TagType.Ice, RarityType.White, 20), + [TagType.FreezeMask] = CreateRule(TagType.FreezeMask, RarityType.White, 20), + [TagType.Shatter] = CreateRule(TagType.Shatter, RarityType.Green, 15), + [TagType.AbsoluteZero] = CreateRule(TagType.AbsoluteZero, RarityType.Purple, 5), + [TagType.Pierce] = CreateRule(TagType.Pierce, RarityType.White, 20), + [TagType.Crit] = CreateRule(TagType.Crit, RarityType.White, 20), + [TagType.Overpenetrate] = CreateRule(TagType.Overpenetrate, RarityType.Green, 15), + [TagType.Execution] = CreateRule(TagType.Execution, RarityType.Purple, 5) + }; + } + + private static TagGenerationRuleData CreateRule(TagType tagType, RarityType minRarity, int weight) + { + return new TagGenerationRuleData + { + TagType = tagType, + MinRarity = minRarity, + Weight = weight + }; + } + + private static void ApplyRow(DRTag row) + { + Debug.Assert(row != null); + Debug.Assert(row.Id > 0); + Debug.Assert(row.Weight > 0); + + RulesByTag[row.TagType] = CreateRule(row.TagType, row.MinRarity, row.Weight); + } + } +} diff --git a/Assets/GameMain/Scripts/Definition/TagGenerationRuleRegistry.cs.meta b/Assets/GameMain/Scripts/Definition/TagGenerationRuleRegistry.cs.meta new file mode 100644 index 0000000..31f3f9d --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/TagGenerationRuleRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2e72e26721274985ba2d44ca825d7f75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs b/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs index 8649028..239d556 100644 --- a/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs +++ b/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs @@ -204,6 +204,15 @@ namespace GeometryTD.Procedure TagConfigRegistry.LoadFromRows(tagConfigTable.GetAllDataRows()); } } + + if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("Tag", false)) + { + var tagTable = GameEntry.DataTable.GetDataTable(); + if (tagTable != null) + { + TagGenerationRuleRegistry.LoadFromRows(tagTable.GetAllDataRows()); + } + } } private void OnLoadDataTableFailure(object sender, GameEventArgs e) diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain.meta new file mode 100644 index 0000000..23aa111 --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f18d07456c02b1546b198ed25ba69e18 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs similarity index 100% rename from Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs diff --git a/Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs.meta rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs.meta diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs similarity index 66% rename from Assets/GameMain/Scripts/Procedure/ProcedureMain.cs rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs index e2989df..5ed0f84 100644 --- a/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs @@ -1,4 +1,3 @@ -using System.Text; using GameFramework.Event; using GameFramework.Fsm; using GameFramework.Procedure; @@ -9,243 +8,6 @@ using UnityGameFramework.Runtime; namespace GeometryTD.Procedure { - public enum ProcedureMainFlowPhase - { - Hub = 0, - NodeActive = 1, - RunCompletedPendingFinish = 2 - } - - public enum ProcedureMainRunAdvanceResult - { - NoChange = 0, - NodeException = 1, - AdvancedToNextNode = 2, - RunCompleted = 3 - } - - public enum ProcedureMainRunCompletionResult - { - NoChange = 0, - ShowCompletionDialog = 1, - 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( - 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; - } - } - - 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; - } - } - - 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); - } - } - - 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; @@ -467,7 +229,9 @@ namespace GeometryTD.Procedure case RunNodeType.BossCombat: ProcedureMainCombatEntryValidationResult validationResult = ProcedureMainCombatEntryValidationService.Validate( - GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null); + GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : null); if (!validationResult.CanEnterCombat) { LogCombatEntryBlocked(currentNode, validationResult); @@ -659,4 +423,4 @@ namespace GeometryTD.Procedure _isReturnToMenuPending = true; } } -} +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Procedure/ProcedureMain.cs.meta rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs.meta diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs new file mode 100644 index 0000000..6173e64 --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs @@ -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; } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs.meta new file mode 100644 index 0000000..0ea22f0 --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5699aa9933294974875bfce1d34d1d4b +timeCreated: 1773198121 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs new file mode 100644 index 0000000..98857ad --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs @@ -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 "不满足当前参战条件。"; + } + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs.meta new file mode 100644 index 0000000..25713fa --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: be3e90d1a1874e26af6d1555c301aa26 +timeCreated: 1773198334 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs new file mode 100644 index 0000000..a342c3a --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs.meta new file mode 100644 index 0000000..7b4235a --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 212a0e12be2248bf9415a779414312a1 +timeCreated: 1773198194 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs new file mode 100644 index 0000000..8559562 --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs.meta new file mode 100644 index 0000000..7c3a125 --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8d565626cc594843989da76a623a1698 +timeCreated: 1773198165 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs new file mode 100644 index 0000000..e0942d7 --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs.meta new file mode 100644 index 0000000..8cb8984 --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f62dcb2d97fe4f0c884e6eab3d500edf +timeCreated: 1773198145 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/RunModel.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs similarity index 100% rename from Assets/GameMain/Scripts/Procedure/RunModel.cs rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs diff --git a/Assets/GameMain/Scripts/Procedure/RunModel.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Procedure/RunModel.cs.meta rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs.meta diff --git a/Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateAdvanceService.cs similarity index 100% rename from Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateAdvanceService.cs diff --git a/Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateAdvanceService.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs.meta rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateAdvanceService.cs.meta diff --git a/Assets/GameMain/Scripts/Procedure/RunStateFactory.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateFactory.cs similarity index 100% rename from Assets/GameMain/Scripts/Procedure/RunStateFactory.cs rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateFactory.cs diff --git a/Assets/GameMain/Scripts/Procedure/RunStateFactory.cs.meta b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateFactory.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Procedure/RunStateFactory.cs.meta rename to Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateFactory.cs.meta diff --git a/Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs b/Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs index 701e05d..be428da 100644 --- a/Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs +++ b/Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs @@ -16,13 +16,10 @@ namespace GeometryTD.UI private readonly List _currentGoods = new List(GoodsCount); private readonly List _shopPriceRows = new List(); - private readonly Dictionary _tagMinRarityByTag = new Dictionary(); - private IDataTable _shopPriceTable; private IDataTable _muzzleCompTable; private IDataTable _bearingCompTable; private IDataTable _baseCompTable; - private IDataTable _tagTable; public bool PrepareForOpen() { @@ -101,9 +98,8 @@ namespace GeometryTD.UI _muzzleCompTable ??= GameEntry.DataTable.GetDataTable(); _bearingCompTable ??= GameEntry.DataTable.GetDataTable(); _baseCompTable ??= GameEntry.DataTable.GetDataTable(); - _tagTable ??= GameEntry.DataTable.GetDataTable(); - if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null || _tagTable == null) + if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null) { Log.Warning("ShopFormUseCase.EnsureTables() failed. Missing required data tables."); return false; @@ -127,13 +123,10 @@ namespace GeometryTD.UI } } - EnsureTagMinRarityLookup(); - return _shopPriceRows.Count > 0 && _muzzleCompTable.Count > 0 && _bearingCompTable.Count > 0 && - _baseCompTable.Count > 0 && - _tagMinRarityByTag.Count > 0; + _baseCompTable.Count > 0; } private bool TryBuildRandomGoodsItem(int goodsIndex, out GoodsItemRawData goodsItem) @@ -197,8 +190,7 @@ namespace GeometryTD.UI normalizedRarity, InventoryTagSourceType.Shop, instanceId, - config.Id, - _tagMinRarityByTag), + config.Id), AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty(), DamageRandomRate = config.DamageRandomRate, AttackMethodType = config.AttackMethodType @@ -224,8 +216,7 @@ namespace GeometryTD.UI normalizedRarity, InventoryTagSourceType.Shop, instanceId, - config.Id, - _tagMinRarityByTag), + config.Id), RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty(), AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty() }; @@ -250,34 +241,12 @@ namespace GeometryTD.UI normalizedRarity, InventoryTagSourceType.Shop, instanceId, - config.Id, - _tagMinRarityByTag), + config.Id), AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty(), AttackPropertyType = config.AttackPropertyType }; } - private void EnsureTagMinRarityLookup() - { - if (_tagMinRarityByTag.Count > 0 || _tagTable == null) - { - return; - } - - DRTag[] rows = _tagTable.GetAllDataRows(); - for (int i = 0; i < rows.Length; i++) - { - DRTag row = rows[i]; - if (row == null || row.Id <= 0) - { - continue; - } - - TagType tagType = (TagType)row.Id; - _tagMinRarityByTag[tagType] = row.MinRarity; - } - } - private int ResolveRandomPrice(RarityType rarity) { for (int i = 0; i < _shopPriceRows.Count; i++) diff --git a/Assets/Tests/EditMode/InventoryTagRuleServiceTests.cs b/Assets/Tests/EditMode/InventoryTagRuleServiceTests.cs index 8ece44f..48a3d5c 100644 --- a/Assets/Tests/EditMode/InventoryTagRuleServiceTests.cs +++ b/Assets/Tests/EditMode/InventoryTagRuleServiceTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using GeometryTD.CustomUtility; +using GeometryTD.DataTable; using GeometryTD.Definition; using NUnit.Framework; @@ -7,16 +8,16 @@ namespace GeometryTD.Tests.EditMode { public sealed class InventoryTagRuleServiceTests { - private static readonly IReadOnlyDictionary MinRarityByTag = - new Dictionary + private static readonly IReadOnlyDictionary RulesByTag = + new Dictionary { - { TagType.Fire, RarityType.White }, - { TagType.Ice, RarityType.White }, - { TagType.Crit, RarityType.White }, - { TagType.Shatter, RarityType.Green }, - { TagType.Inferno, RarityType.Purple }, - { TagType.AbsoluteZero, RarityType.Purple }, - { TagType.Execution, RarityType.Purple }, + { TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 20 } }, + { TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 20 } }, + { TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 20 } }, + { TagType.Shatter, new TagGenerationRuleData { TagType = TagType.Shatter, MinRarity = RarityType.Green, Weight = 15 } }, + { TagType.Inferno, new TagGenerationRuleData { TagType = TagType.Inferno, MinRarity = RarityType.Purple, Weight = 5 } }, + { TagType.AbsoluteZero, new TagGenerationRuleData { TagType = TagType.AbsoluteZero, MinRarity = RarityType.Purple, Weight = 5 } }, + { TagType.Execution, new TagGenerationRuleData { TagType = TagType.Execution, MinRarity = RarityType.Purple, Weight = 5 } }, }; [Test] @@ -33,7 +34,7 @@ namespace GeometryTD.Tests.EditMode (TagType)99 }, RarityType.Green, - MinRarityByTag); + RulesByTag); Assert.That(result, Is.EqualTo(new[] { TagType.Fire })); } @@ -47,7 +48,7 @@ namespace GeometryTD.Tests.EditMode InventoryTagSourceType.Shop, 12345, 7, - MinRarityByTag); + RulesByTag); TagType[] second = InventoryTagRuleService.ResolveComponentTags( new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter }, @@ -55,7 +56,7 @@ namespace GeometryTD.Tests.EditMode InventoryTagSourceType.Shop, 12345, 7, - MinRarityByTag); + RulesByTag); Assert.That(second, Is.EqualTo(first)); } @@ -69,7 +70,7 @@ namespace GeometryTD.Tests.EditMode InventoryTagSourceType.Drop, 9001, 4, - MinRarityByTag); + RulesByTag); Assert.That(result.Length, Is.EqualTo(2)); Assert.That(new HashSet(result).Count, Is.EqualTo(result.Length)); @@ -84,11 +85,82 @@ namespace GeometryTD.Tests.EditMode InventoryTagSourceType.Seed, 42, 1, - MinRarityByTag); + RulesByTag); Assert.That(result, Is.EqualTo(new[] { TagType.Fire })); } + [Test] + public void ResolveComponentTags_Uses_Weight_Within_EligiblePool() + { + IReadOnlyDictionary weightedRules = + new Dictionary + { + { TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 100 } }, + { TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 1 } }, + { TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 1 } }, + }; + + TagType[] result = InventoryTagRuleService.ResolveComponentTags( + new[] { TagType.Fire, TagType.Ice, TagType.Crit }, + RarityType.Green, + InventoryTagSourceType.Shop, + 1, + 1, + weightedRules); + + Assert.That(result, Has.Length.EqualTo(1)); + Assert.That(result[0], Is.EqualTo(TagType.Fire)); + } + + [Test] + public void ResolveComponentTags_Prefers_HighWeight_Tag_In_ExpandedPool() + { + IReadOnlyDictionary weightedRules = + new Dictionary + { + { TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 500 } }, + { TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 1 } }, + { TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 1 } }, + { TagType.Shatter, new TagGenerationRuleData { TagType = TagType.Shatter, MinRarity = RarityType.White, Weight = 1 } }, + }; + + int fireCount = 0; + for (int i = 0; i < 32; i++) + { + TagType[] result = InventoryTagRuleService.ResolveComponentTags( + new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter }, + RarityType.Green, + InventoryTagSourceType.Shop, + 1000 + i, + 1, + weightedRules); + + Assert.That(result, Has.Length.EqualTo(1)); + if (result[0] == TagType.Fire) + { + fireCount++; + } + } + + Assert.That(fireCount, Is.GreaterThanOrEqualTo(28)); + } + + [Test] + public void TagGenerationRuleRegistry_LoadFromRows_Overrides_MinRarity_And_Weight() + { + DRTag fireRow = new DRTag(); + Assert.That(fireRow.ParseDataRow("\t1\t元素\tFire\tGreen\t33", null), Is.True); + + TagGenerationRuleRegistry.LoadFromRows(new[] { fireRow }); + + Assert.That(TagGenerationRuleRegistry.TryGetRule(TagType.Fire, out TagGenerationRuleData rule), Is.True); + Assert.That(rule.MinRarity, Is.EqualTo(RarityType.Green)); + Assert.That(rule.Weight, Is.EqualTo(33)); + + TagGenerationRuleRegistry.ResetToDefaults(); + } + [Test] public void CreateSampleInventory_Generates_SeedTags_Within_Launch_Set_And_Matches_Tower_Stats() { diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index 1690080..8170546 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -148,6 +148,10 @@ > 2026-03-10 更新:`S4-07` 仍未开始正式收口。当前 Tag 参数仍主要承载在代码侧 `TagConfigRegistry` 与各 Tag 配置类中,`Tag.txt` / DataTable 仍只提供基础字典与 `MinRarity` 输入;文档中约定的 `TagRule` 表、触发阶段、权重、效果参数等尚未形成 DataRow 与运行时消费闭环。因此 `S4-07` 继续保持未完成状态。 > > 2026-03-11 更新:`S4-07` 已进入第一阶段实现。当前已新增 `TagConfig.txt` 与 `DRTagConfig`,`ProcedurePreload` 会在加载 `TagConfig` 表后驱动 `TagConfigRegistry.LoadFromRows(...)`,把 `TriggerPhase`、`Description` 以及首发 7 个 Tag 的配置参数从表覆盖到运行时强类型 `TagConfig`。`ItemDescForm` 也已开始消费该配置说明;塔详情会优先使用 `TagRuntimes` 构建 `Ice x2` 这类叠层展示。因此 `S4-07` 不再是“完全未开始”,但当前仍只完成了 `TagConfig` 级别的参数映射与 UI 消费,尚未把文档中的完整 `TagRule`(如权重、生成规则等)全部收口。 +> +> 2026-03-11 更新:`S4-07` 已继续推进到第二阶段。当前 `Tag.txt -> DRTag -> TagGenerationRuleRegistry` 已形成组件 Tag 生成规则闭环,`InventoryTagRuleService` 不再主要依赖内部 `MinRarity` 硬编码,而是统一消费 `DRTag` 提供的 `MinRarity + Weight`;`ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 也已切回这一统一入口。现阶段的职责边界明确为:`Tag.txt` 负责基础字典与生成规则,`TagConfig.txt` 负责触发阶段、描述与效果参数。 +> +> 2026-03-11 补充验证:已通过手工扩充组件 `PossibleTag` 候选池并调整 `Weight` 验证生成权重实际生效;同时把所有 Tag 的 `MinRarity` 提升到 `Red` 后,低品质组件不再生成任何 Tag,说明 `Tag.txt` 中的 `MinRarity` 与 `Weight` 均已进入统一生成链。 ### S4-01 边界结论 @@ -183,7 +187,8 @@ - `Shatter` 已接入命中前数值修正链;`Inferno`、`AbsoluteZero` 已按“强化现有 `Fire` / `Ice` 状态”的首发口径落地,状态类 Tag 的内部结构继续保持注册式运行时。 - `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 已进入分类与配置骨架,但仍属于后续扩展,不计入当前 `S4` 的完成标准。 - `S4-07` 已进入第一阶段实现。当前仓库已具备 `TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 的消费闭环,首发 7 个 Tag 的触发阶段、描述与核心参数已可由表覆盖。 -- `S4-07` 仍未完成最终收口。当前采用的是 `TagConfig` 表驱动,而不是文档原方案里的完整 `TagRule`;权重、生成规则等字段仍未全部映射回 DataTable / DataRow / 运行时消费链。 +- `S4-07` 已推进到第二阶段。当前仓库同时具备 `Tag.txt -> DRTag -> TagGenerationRuleRegistry -> InventoryTagRuleService` 与 `TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 两条消费闭环,首发 7 个 Tag 的生成规则、触发阶段、描述与核心参数都已开始由表驱动。 +- `S4-07` 仍未完成最终收口。当前采用的是 `Tag.txt + TagConfig.txt` 的分层方案,而不是文档原方案里的单独 `TagRule`;更深的规则字段与完整命名口径仍待后续决定是否继续统一。 ### S4-06 当前代码状态 @@ -201,8 +206,8 @@ ### S4 后续执行计划 -1. 继续推进 `S4-07`:在现有 `TagConfig` 表驱动基础上,决定是否补成文档中的完整 `TagRule`,或明确将 `TagConfig` 作为当前阶段的等价收口方案。 -2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的参数和说明配置化,后续重点转向剩余规则字段是否也要配置化。 +1. 继续推进 `S4-07`:在现有 `Tag.txt + TagConfig.txt` 分层方案基础上,决定是否补成文档中的完整 `TagRule`,或明确当前双表就是本阶段的等价收口方案。 +2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的生成规则、参数和说明配置化,后续重点转向是否还要继续配置化更深层的规则字段。 3. `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 继续留在后续扩展阶段,不因为 `S4-06` 完成而提前进入当前迭代。 ## 阶段 S5 - 收口耐久规则 diff --git a/数据表/Tag.xlsx b/数据表/Tag.xlsx index a6607b72af2cb00668738ffaf7f1b4a8727fa6da..9b238834a2a07b0dfe4d97a984556d4e932ba356 100644 GIT binary patch delta 8679 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 delta 8266 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 De&c#w diff --git a/数据表/TowerComp.xlsx b/数据表/TowerComp.xlsx index 991d4610efd0ea4948839ea00dc7e07c84685dc0..e1caaeb86e59f1d9939e8c97ead952159997d799 100644 GIT binary patch delta 7560 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 U@Bfh2NdA$al4R%^!hduB7d}S_&Hw-a delta 7269 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 BXx{(; diff --git a/数据表/convert.py b/数据表/convert.py index 4a83ecc..c8a034c 100644 --- a/数据表/convert.py +++ b/数据表/convert.py @@ -36,7 +36,12 @@ def convert_excel_to_txt(folder_path='.'): excel_file = pd.ExcelFile(file_path) valid_sheets = [] for sheet_name in excel_file.sheet_names: - df = pd.read_excel(excel_file, sheet_name=sheet_name, header=None) + df = pd.read_excel( + excel_file, + sheet_name=sheet_name, + header=None, + keep_default_na=False, + ) if is_valid_sheet(df): valid_sheets.append((sheet_name, df))