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 a6607b7..9b23883 100644 Binary files a/数据表/Tag.xlsx and b/数据表/Tag.xlsx differ diff --git a/数据表/TowerComp.xlsx b/数据表/TowerComp.xlsx index 991d461..e1caaeb 100644 Binary files a/数据表/TowerComp.xlsx and b/数据表/TowerComp.xlsx differ 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))