diff --git a/.gitignore b/.gitignore index aceb572..27cf066 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,5 @@ InitTestScene*.unity* /[Aa]ssets/RawResources.meta /.dotnet /.idea -~$*.xlsx \ No newline at end of file +~$*.xlsx +/tools \ No newline at end of file diff --git a/Assets/GameMain/DataTables/Tag.txt b/Assets/GameMain/DataTables/Tag.txt index 2044cb5..f07dbaa 100644 --- a/Assets/GameMain/DataTables/Tag.txt +++ b/Assets/GameMain/DataTables/Tag.txt @@ -1,5 +1,4 @@ -# Tag表 -# Id Name MinRarity +# Id 列1 Name MinRarity # int string RarityType # Tag编号 策划备注 Tag名 获取最低稀有度 1 元素 Fire White diff --git a/Assets/GameMain/DataTables/TagConfig.txt b/Assets/GameMain/DataTables/TagConfig.txt new file mode 100644 index 0000000..99c6a28 --- /dev/null +++ b/Assets/GameMain/DataTables/TagConfig.txt @@ -0,0 +1,51 @@ +# 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\ + } diff --git a/Assets/GameMain/DataTables/TagConfig.txt.meta b/Assets/GameMain/DataTables/TagConfig.txt.meta new file mode 100644 index 0000000..6d7d160 --- /dev/null +++ b/Assets/GameMain/DataTables/TagConfig.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 237e82d08a4faa64da2401808bc652fc +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/DataTables/UIForm.txt b/Assets/GameMain/DataTables/UIForm.txt index accf4d2..7aa8719 100644 --- a/Assets/GameMain/DataTables/UIForm.txt +++ b/Assets/GameMain/DataTables/UIForm.txt @@ -7,7 +7,7 @@ 102 关于 AboutForm Medium False True 110 主界面 MainForm Medium False False 111 仓库UI RepoForm Medium False False - 112 大地图UI NodeMapForm Medium False True + 112 大地图UI NodeMapForm Medium False False 113 详细信息 ItemDescForm Medium True False 114 奖励选择UI RewardSelectForm Medium False True 130 事件UI EventForm Medium False True diff --git a/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs b/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs new file mode 100644 index 0000000..b8dde6a --- /dev/null +++ b/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs @@ -0,0 +1,41 @@ +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + public sealed class DRTagConfig : DataRowBase + { + private int m_Id; + + public override int Id => m_Id; + + public TagType TagType { get; private set; } + + public TagTriggerPhase TriggerPhase { get; private set; } + + public string Description { get; private set; } + + public string ParamJson { get; private set; } + + public override bool ParseDataRow(string dataRowString, object userData) + { + string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators); + for (int i = 0; i < columnStrings.Length; i++) + { + columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators); + } + + int index = 0; + index++; + m_Id = int.Parse(columnStrings[index++]); + index++; + TagType = EnumUtility.Get(columnStrings[index++]); + TriggerPhase = EnumUtility.Get(columnStrings[index++]); + Description = columnStrings[index++]; + ParamJson = columnStrings[index++]; + + return true; + } + } +} diff --git a/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs.meta b/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs.meta new file mode 100644 index 0000000..1d3cbe3 --- /dev/null +++ b/Assets/GameMain/Scripts/DataTable/DRTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c3cd426c0ec84c0bbfd9918d768464f9 +timeCreated: 1773139200 diff --git a/Assets/GameMain/Scripts/Definition/DataStruct/TowerItemData.cs b/Assets/GameMain/Scripts/Definition/DataStruct/TowerItemData.cs index 580be83..d2ba619 100644 --- a/Assets/GameMain/Scripts/Definition/DataStruct/TowerItemData.cs +++ b/Assets/GameMain/Scripts/Definition/DataStruct/TowerItemData.cs @@ -4,24 +4,6 @@ using UnityEngine; namespace GeometryTD.Definition { - /// - /// 防御塔独立属性快照。 - /// 注意:这里是塔实例的独立值,不通过组件引用实时计算。 - /// - [Serializable] - public sealed class TowerStatsData - { - public int[] AttackDamage { get; set; } - public float DamageRandomRate { get; set; } - public float[] RotateSpeed { get; set; } - public float[] AttackRange { get; set; } - public float[] AttackSpeed { get; set; } - public AttackMethodType AttackMethodType { get; set; } - public AttackPropertyType AttackPropertyType { get; set; } - public TagRuntimeData[] TagRuntimes { get; set; } - public TagType[] Tags { get; set; } - } - /// /// 背包内防御塔实例数据。 /// @@ -69,4 +51,4 @@ namespace GeometryTD.Definition [JsonIgnore] public string ComposedIconKey { get; set; } } -} +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/DataStruct/TowerStatsData.cs b/Assets/GameMain/Scripts/Definition/DataStruct/TowerStatsData.cs new file mode 100644 index 0000000..e23379a --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/DataStruct/TowerStatsData.cs @@ -0,0 +1,22 @@ +using System; + +namespace GeometryTD.Definition +{ + /// + /// 防御塔独立属性快照。 + /// 注意:这里是塔实例的独立值,不通过组件引用实时计算。 + /// + [Serializable] + public sealed class TowerStatsData + { + public int[] AttackDamage { get; set; } + public float DamageRandomRate { get; set; } + public float[] RotateSpeed { get; set; } + public float[] AttackRange { get; set; } + public float[] AttackSpeed { get; set; } + public AttackMethodType AttackMethodType { get; set; } + public AttackPropertyType AttackPropertyType { get; set; } + public TagRuntimeData[] TagRuntimes { get; set; } + public TagType[] Tags { get; set; } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/DataStruct/TowerStatsData.cs.meta b/Assets/GameMain/Scripts/Definition/DataStruct/TowerStatsData.cs.meta new file mode 100644 index 0000000..b8fd6c8 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/DataStruct/TowerStatsData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 33934c152fda4ff4827aabfbf6f65856 +timeCreated: 1773194446 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs index ffc144e..b93f9b9 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs @@ -21,15 +21,19 @@ namespace GeometryTD.Definition } FireTagState state = runtime.GetOrCreateState(TagType); - float burnDamagePerSecond = Mathf.Max(0f, - config.BurnDamagePerSecondPerStack * runtimeData.TotalStack); + int infernoStack = TagEffectResolver.GetTagStack(attackPayload?.TagRuntimes, TagType.Inferno); + float burnDamagePerSecond = Mathf.Max( + 0f, + config.BurnDamagePerSecondPerStack * runtimeData.TotalStack + GetInfernoBonusDamagePerSecond(infernoStack)); if (burnDamagePerSecond <= 0f) { return; } - state.RemainingDuration = Mathf.Max(state.RemainingDuration, config.BurnDurationSeconds); + state.RemainingDuration = Mathf.Max( + state.RemainingDuration, + config.BurnDurationSeconds + GetInfernoBonusDuration(infernoStack)); state.DamagePerSecond = Mathf.Max(state.DamagePerSecond, burnDamagePerSecond); runtime.Activate(TagType); } @@ -64,5 +68,39 @@ namespace GeometryTD.Definition state.PendingDamage = 0f; return false; } + + private static float GetInfernoBonusDuration(int infernoStack) + { + if (infernoStack <= 0 || + !TagConfigRegistry.TryGetDefinition(TagType.Inferno, out TagDefinitionData definition)) + { + return 0f; + } + + InfernoTagConfig infernoConfig = definition.Config as InfernoTagConfig; + if (infernoConfig == null || !infernoConfig.IsImplemented) + { + return 0f; + } + + return infernoStack * infernoConfig.BonusBurnDurationSeconds; + } + + private static float GetInfernoBonusDamagePerSecond(int infernoStack) + { + if (infernoStack <= 0 || + !TagConfigRegistry.TryGetDefinition(TagType.Inferno, out TagDefinitionData definition)) + { + return 0f; + } + + InfernoTagConfig infernoConfig = definition.Config as InfernoTagConfig; + if (infernoConfig == null || !infernoConfig.IsImplemented) + { + return 0f; + } + + return infernoStack * infernoConfig.BonusBurnDamagePerSecondPerStack; + } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs index d1cf3eb..3cada12 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs @@ -24,9 +24,12 @@ namespace GeometryTD.Definition } IceTagState state = runtime.GetOrCreateState(TagType); - float slowMultiplier = Mathf.Max(config.MinMoveSpeedMultiplier, - 1f - runtimeData.TotalStack * config.SlowRatioPerStack); - state.RemainingDuration = Mathf.Max(state.RemainingDuration, config.SlowDurationSeconds); + int absoluteZeroStack = TagEffectResolver.GetTagStack(attackPayload?.TagRuntimes, TagType.AbsoluteZero); + float slowRatio = runtimeData.TotalStack * config.SlowRatioPerStack + GetAbsoluteZeroBonusSlowRatio(absoluteZeroStack); + float slowMultiplier = Mathf.Max(config.MinMoveSpeedMultiplier, 1f - slowRatio); + state.RemainingDuration = Mathf.Max( + state.RemainingDuration, + config.SlowDurationSeconds + GetAbsoluteZeroBonusDuration(absoluteZeroStack)); state.SlowMultiplier = Mathf.Min(state.SlowMultiplier, slowMultiplier); runtime.Activate(TagType); } @@ -46,5 +49,39 @@ namespace GeometryTD.Definition IceTagState state = runtime.GetState(TagType); return state == null ? 1f : state.SlowMultiplier; } + + private static float GetAbsoluteZeroBonusDuration(int absoluteZeroStack) + { + if (absoluteZeroStack <= 0 || + !TagConfigRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinitionData definition)) + { + return 0f; + } + + AbsoluteZeroTagConfig absoluteZeroConfig = definition.Config as AbsoluteZeroTagConfig; + if (absoluteZeroConfig == null || !absoluteZeroConfig.IsImplemented) + { + return 0f; + } + + return absoluteZeroStack * absoluteZeroConfig.BonusSlowDurationSeconds; + } + + private static float GetAbsoluteZeroBonusSlowRatio(int absoluteZeroStack) + { + if (absoluteZeroStack <= 0 || + !TagConfigRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinitionData definition)) + { + return 0f; + } + + AbsoluteZeroTagConfig absoluteZeroConfig = definition.Config as AbsoluteZeroTagConfig; + if (absoluteZeroConfig == null || !absoluteZeroConfig.IsImplemented) + { + return 0f; + } + + return absoluteZeroStack * absoluteZeroConfig.BonusSlowRatioPerStack; + } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs index 6ff564e..1cbe5b2 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs @@ -9,7 +9,7 @@ namespace GeometryTD.Definition { } - public float BonusSlowDurationSeconds { get; set; } = 0f; - public float BonusSlowRatioPerStack { get; set; } = 0f; + public float BonusSlowDurationSeconds { get; set; } = 1f; + public float BonusSlowRatioPerStack { get; set; } = 0.1f; } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs index 5b80c04..66fedd4 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs @@ -11,5 +11,6 @@ namespace GeometryTD.Definition public float BurnDurationSeconds { get; set; } = 3f; public float BurnDamagePerSecondPerStack { get; set; } = 20f; + public int MaxEffectiveStack { get; set; } = 5; } -} +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs index 416390c..82e6806 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs @@ -9,7 +9,7 @@ namespace GeometryTD.Definition { } - public float BonusBurnDurationSeconds { get; set; } = 0f; - public float BonusBurnDamagePerSecondPerStack { get; set; } = 0f; + public float BonusBurnDurationSeconds { get; set; } = 2f; + public float BonusBurnDamagePerSecondPerStack { get; set; } = 20f; } } diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs index e29ecc9..b7b9b93 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs @@ -9,6 +9,6 @@ namespace GeometryTD.Definition { } - public int ExtraPierceCount { get; set; } = 0; + public int ExtraPierceCount { get; set; } = 2; } } \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs index 10c1f0c..c1ac605 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs @@ -10,6 +10,6 @@ namespace GeometryTD.Definition } public bool RequiresSlowedTarget { get; set; } = true; - public float DamageBonusPerStack { get; set; } = 0f; + public float DamageBonusPerStack { get; set; } = 0.25f; } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs index 9288457..3945c46 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs @@ -1,103 +1,174 @@ using System.Collections.Generic; +using GeometryTD.DataTable; +using Newtonsoft.Json.Linq; +using UnityEngine; namespace GeometryTD.Definition { public static class TagConfigRegistry { - private static readonly Dictionary DefinitionsByTag = - new Dictionary - { - [TagType.Fire] = new TagDefinitionData - { - TagType = TagType.Fire, - Category = TagCategory.Status, - TriggerPhase = TagTriggerPhase.OnAfterHit, - Config = new FireTagConfig(true) - }, - [TagType.BurnSpread] = new TagDefinitionData - { - TagType = TagType.BurnSpread, - Category = TagCategory.AttackShape, - TriggerPhase = TagTriggerPhase.OnKill, - Config = new BurnSpreadTagConfig(false) - }, - [TagType.IgniteBurst] = new TagDefinitionData - { - TagType = TagType.IgniteBurst, - Category = TagCategory.AttackShape, - TriggerPhase = TagTriggerPhase.OnKill, - Config = new IgniteBurstTagConfig(false) - }, - [TagType.Inferno] = new TagDefinitionData - { - TagType = TagType.Inferno, - Category = TagCategory.Status, - TriggerPhase = TagTriggerPhase.OnAfterHit, - Config = new InfernoTagConfig(false) - }, - [TagType.Ice] = new TagDefinitionData - { - TagType = TagType.Ice, - Category = TagCategory.Status, - TriggerPhase = TagTriggerPhase.OnAfterHit, - Config = new IceTagConfig(true) - }, - [TagType.FreezeMask] = new TagDefinitionData - { - TagType = TagType.FreezeMask, - Category = TagCategory.AttackShape, - TriggerPhase = TagTriggerPhase.OnAfterHit, - Config = new FreezeMaskTagConfig(false) - }, - [TagType.Shatter] = new TagDefinitionData - { - TagType = TagType.Shatter, - Category = TagCategory.NumericModifier, - TriggerPhase = TagTriggerPhase.OnBeforeHit, - Config = new ShatterTagConfig(false) - }, - [TagType.AbsoluteZero] = new TagDefinitionData - { - TagType = TagType.AbsoluteZero, - Category = TagCategory.Status, - TriggerPhase = TagTriggerPhase.OnAfterHit, - Config = new AbsoluteZeroTagConfig(false) - }, - [TagType.Pierce] = new TagDefinitionData - { - TagType = TagType.Pierce, - Category = TagCategory.AttackShape, - TriggerPhase = TagTriggerPhase.OnHit, - Config = new PierceTagConfig(false) - }, - [TagType.Crit] = new TagDefinitionData - { - TagType = TagType.Crit, - Category = TagCategory.NumericModifier, - TriggerPhase = TagTriggerPhase.OnBeforeHit, - Config = new CritTagConfig(true) - }, - [TagType.Overpenetrate] = new TagDefinitionData - { - TagType = TagType.Overpenetrate, - Category = TagCategory.AttackShape, - TriggerPhase = TagTriggerPhase.OnHit, - Config = new OverpenetrateTagConfig(false) - }, - [TagType.Execution] = new TagDefinitionData - { - TagType = TagType.Execution, - Category = TagCategory.NumericModifier, - TriggerPhase = TagTriggerPhase.OnBeforeHit, - Config = new ExecutionTagConfig(true) - } - }; + private static readonly Dictionary DefinitionsByTag = CreateDefaultDefinitions(); public static IReadOnlyDictionary Definitions => DefinitionsByTag; + public static void ResetToDefaults() + { + DefinitionsByTag.Clear(); + foreach (KeyValuePair pair in CreateDefaultDefinitions()) + { + DefinitionsByTag.Add(pair.Key, pair.Value); + } + } + + public static void LoadFromRows(IEnumerable rows) + { + ResetToDefaults(); + foreach (DRTagConfig row in rows) + { + ApplyRow(row); + } + } + public static bool TryGetDefinition(TagType tagType, out TagDefinitionData definition) { return DefinitionsByTag.TryGetValue(tagType, out definition); } + + private static Dictionary CreateDefaultDefinitions() + { + return new Dictionary + { + [TagType.Fire] = CreateDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit, new FireTagConfig(true)), + [TagType.BurnSpread] = CreateDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.OnKill, new BurnSpreadTagConfig(false)), + [TagType.IgniteBurst] = CreateDefinition(TagType.IgniteBurst, TagCategory.AttackShape, TagTriggerPhase.OnKill, new IgniteBurstTagConfig(false)), + [TagType.Inferno] = CreateDefinition(TagType.Inferno, TagCategory.Status, TagTriggerPhase.OnAfterHit, new InfernoTagConfig(true)), + [TagType.Ice] = CreateDefinition(TagType.Ice, TagCategory.Status, TagTriggerPhase.OnAfterHit, new IceTagConfig(true)), + [TagType.FreezeMask] = CreateDefinition(TagType.FreezeMask, TagCategory.AttackShape, TagTriggerPhase.OnAfterHit, new FreezeMaskTagConfig(false)), + [TagType.Shatter] = CreateDefinition(TagType.Shatter, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ShatterTagConfig(true)), + [TagType.AbsoluteZero] = CreateDefinition(TagType.AbsoluteZero, TagCategory.Status, TagTriggerPhase.OnAfterHit, new AbsoluteZeroTagConfig(true)), + [TagType.Pierce] = CreateDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.OnHit, new PierceTagConfig(false)), + [TagType.Crit] = CreateDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new CritTagConfig(true)), + [TagType.Overpenetrate] = CreateDefinition(TagType.Overpenetrate, TagCategory.AttackShape, TagTriggerPhase.OnHit, new OverpenetrateTagConfig(false)), + [TagType.Execution] = CreateDefinition(TagType.Execution, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ExecutionTagConfig(true)) + }; + } + + private static TagDefinitionData CreateDefinition( + TagType tagType, + TagCategory category, + TagTriggerPhase triggerPhase, + TagConfigBase config) + { + return new TagDefinitionData + { + TagType = tagType, + Category = category, + TriggerPhase = triggerPhase, + Description = string.Empty, + Config = config + }; + } + + private static void ApplyRow(DRTagConfig row) + { + Debug.Assert(row != null); + Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinitionData definition)); + + definition.TriggerPhase = row.TriggerPhase; + definition.Description = row.Description ?? string.Empty; + + if (string.IsNullOrWhiteSpace(row.ParamJson)) + { + return; + } + + JObject param = JObject.Parse(row.ParamJson); + switch (row.TagType) + { + case TagType.Fire: + ApplyFireConfig((FireTagConfig)definition.Config, param); + break; + case TagType.Inferno: + ApplyInfernoConfig((InfernoTagConfig)definition.Config, param); + break; + case TagType.Ice: + ApplyIceConfig((IceTagConfig)definition.Config, param); + break; + case TagType.Shatter: + ApplyShatterConfig((ShatterTagConfig)definition.Config, param); + break; + case TagType.AbsoluteZero: + ApplyAbsoluteZeroConfig((AbsoluteZeroTagConfig)definition.Config, param); + break; + case TagType.Crit: + ApplyCritConfig((CritTagConfig)definition.Config, param); + break; + case TagType.Execution: + ApplyExecutionConfig((ExecutionTagConfig)definition.Config, param); + break; + } + } + + private static void ApplyFireConfig(FireTagConfig config, JObject param) + { + config.BurnDurationSeconds = ReadFloat(param, nameof(FireTagConfig.BurnDurationSeconds), config.BurnDurationSeconds); + config.BurnDamagePerSecondPerStack = ReadFloat(param, nameof(FireTagConfig.BurnDamagePerSecondPerStack), config.BurnDamagePerSecondPerStack); + config.MaxEffectiveStack = ReadInt(param, nameof(FireTagConfig.MaxEffectiveStack), config.MaxEffectiveStack); + } + + private static void ApplyInfernoConfig(InfernoTagConfig config, JObject param) + { + config.BonusBurnDurationSeconds = ReadFloat(param, nameof(InfernoTagConfig.BonusBurnDurationSeconds), config.BonusBurnDurationSeconds); + config.BonusBurnDamagePerSecondPerStack = ReadFloat(param, nameof(InfernoTagConfig.BonusBurnDamagePerSecondPerStack), config.BonusBurnDamagePerSecondPerStack); + } + + private static void ApplyIceConfig(IceTagConfig config, JObject param) + { + config.SlowDurationSeconds = ReadFloat(param, nameof(IceTagConfig.SlowDurationSeconds), config.SlowDurationSeconds); + config.SlowRatioPerStack = ReadFloat(param, nameof(IceTagConfig.SlowRatioPerStack), config.SlowRatioPerStack); + config.MinMoveSpeedMultiplier = ReadFloat(param, nameof(IceTagConfig.MinMoveSpeedMultiplier), config.MinMoveSpeedMultiplier); + } + + private static void ApplyShatterConfig(ShatterTagConfig config, JObject param) + { + config.RequiresSlowedTarget = ReadBool(param, nameof(ShatterTagConfig.RequiresSlowedTarget), config.RequiresSlowedTarget); + config.DamageBonusPerStack = ReadFloat(param, nameof(ShatterTagConfig.DamageBonusPerStack), config.DamageBonusPerStack); + } + + private static void ApplyAbsoluteZeroConfig(AbsoluteZeroTagConfig config, JObject param) + { + config.BonusSlowDurationSeconds = ReadFloat(param, nameof(AbsoluteZeroTagConfig.BonusSlowDurationSeconds), config.BonusSlowDurationSeconds); + config.BonusSlowRatioPerStack = ReadFloat(param, nameof(AbsoluteZeroTagConfig.BonusSlowRatioPerStack), config.BonusSlowRatioPerStack); + } + + private static void ApplyCritConfig(CritTagConfig config, JObject param) + { + config.CritChancePerStack = ReadFloat(param, nameof(CritTagConfig.CritChancePerStack), config.CritChancePerStack); + config.CritDamageMultiplier = ReadFloat(param, nameof(CritTagConfig.CritDamageMultiplier), config.CritDamageMultiplier); + } + + private static void ApplyExecutionConfig(ExecutionTagConfig config, JObject param) + { + config.TargetHealthThreshold = ReadFloat(param, nameof(ExecutionTagConfig.TargetHealthThreshold), config.TargetHealthThreshold); + config.DamageBonusPerStack = ReadFloat(param, nameof(ExecutionTagConfig.DamageBonusPerStack), config.DamageBonusPerStack); + } + + private static float ReadFloat(JObject param, string key, float defaultValue) + { + JToken token = param[key]; + return token == null ? defaultValue : token.Value(); + } + + private static int ReadInt(JObject param, string key, int defaultValue) + { + JToken token = param[key]; + return token == null ? defaultValue : token.Value(); + } + + private static bool ReadBool(JObject param, string key, bool defaultValue) + { + JToken token = param[key]; + return token == null ? defaultValue : token.Value(); + } } } diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs b/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs index 033e7c6..f53eeb5 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs @@ -8,6 +8,7 @@ namespace GeometryTD.Definition public TagType TagType { get; set; } public TagCategory Category { get; set; } public TagTriggerPhase TriggerPhase { get; set; } + public string Description { get; set; } public TagConfigBase Config { get; set; } } } diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs index 952c7fa..b0ba82d 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs @@ -98,10 +98,18 @@ namespace GeometryTD.Definition private static void ApplyShatter(HitContext hitContext, int stack, bool targetHasSlowStatus, ShatterTagConfig config) { - _ = hitContext; - _ = stack; - _ = targetHasSlowStatus; - _ = config; + if (config == null || !config.IsImplemented || stack <= 0) + { + return; + } + + if (config.RequiresSlowedTarget && !targetHasSlowStatus) + { + return; + } + + float multiplier = 1f + stack * config.DamageBonusPerStack; + hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier)); } } } diff --git a/Assets/GameMain/Scripts/Definition/TowerTagAggregationService.cs b/Assets/GameMain/Scripts/Definition/TowerTagAggregationService.cs index 52bc8ab..38d2934 100644 --- a/Assets/GameMain/Scripts/Definition/TowerTagAggregationService.cs +++ b/Assets/GameMain/Scripts/Definition/TowerTagAggregationService.cs @@ -10,17 +10,15 @@ namespace GeometryTD.Definition Dictionary stackByTag = new Dictionary(); if (componentTags != null) { - for (int i = 0; i < componentTags.Length; i++) + foreach (var tags in componentTags) { - TagType[] tags = componentTags[i]; if (tags == null) { continue; } - for (int j = 0; j < tags.Length; j++) + foreach (var tagType in tags) { - TagType tagType = tags[j]; if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType)) { continue; @@ -44,7 +42,7 @@ namespace GeometryTD.Definition } List runtimes = new List(stackByTag.Count); - foreach (KeyValuePair pair in stackByTag) + foreach (var pair in stackByTag) { runtimes.Add(new TagRuntimeData { @@ -65,9 +63,8 @@ namespace GeometryTD.Definition } HashSet uniqueTags = new HashSet(); - for (int i = 0; i < tags.Count; i++) + foreach (var tagType in tags) { - TagType tagType = tags[i]; if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType)) { continue; @@ -106,9 +103,8 @@ namespace GeometryTD.Definition } List tags = new List(tagRuntimes.Count); - for (int i = 0; i < tagRuntimes.Count; i++) + foreach (var runtime in tagRuntimes) { - TagRuntimeData runtime = tagRuntimes[i]; if (runtime == null || runtime.TagType == TagType.None || !Enum.IsDefined(typeof(TagType), runtime.TagType)) { continue; diff --git a/Assets/GameMain/Scripts/Entity/EntityData/TowerData.cs b/Assets/GameMain/Scripts/Entity/EntityData/TowerData.cs index fd4b23d..e230f2d 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/TowerData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/TowerData.cs @@ -7,11 +7,11 @@ namespace GeometryTD.Entity.EntityData [Serializable] public class TowerData : EntityDataBase { - [SerializeField] private TowerStatsData _stats = new TowerStatsData(); + [SerializeField] private TowerStatsData _stats; [SerializeField] private int _towerLevel = 0; - [SerializeField] private Color _muzzleColor = Color.white; - [SerializeField] private Color _bearingColor = Color.white; - [SerializeField] private Color _baseColor = Color.white; + [SerializeField] private Color _muzzleColor; + [SerializeField] private Color _bearingColor; + [SerializeField] private Color _baseColor; public TowerData(int entityId, int typeId, Vector3 position, Quaternion rotation, TowerStatsData stats, int towerLevel = 0, Color? muzzleColor = null, Color? bearingColor = null, @@ -57,4 +57,4 @@ namespace GeometryTD.Entity.EntityData set => _baseColor = value; } } -} +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs b/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs index c0ea2c4..8649028 100644 --- a/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs +++ b/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs @@ -31,6 +31,7 @@ namespace GeometryTD.Procedure "ShopPrice", "Sound", "Tag", + "TagConfig", "OutGameDropPool", "UIForm", "UISound", @@ -194,6 +195,15 @@ namespace GeometryTD.Procedure _loadedFlag[ne.DataTableAssetName] = true; Log.Info("Load data table '{0}' OK.", ne.DataTableAssetName); + + if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("TagConfig", false)) + { + var tagConfigTable = GameEntry.DataTable.GetDataTable(); + if (tagConfigTable != null) + { + TagConfigRegistry.LoadFromRows(tagConfigTable.GetAllDataRows()); + } + } } private void OnLoadDataTableFailure(object sender, GameEventArgs e) diff --git a/Assets/GameMain/Scripts/UI/Combat/Controller/CombatFinishFormController.cs b/Assets/GameMain/Scripts/UI/Combat/Controller/CombatFinishFormController.cs index a7e0246..fa1dd43 100644 --- a/Assets/GameMain/Scripts/UI/Combat/Controller/CombatFinishFormController.cs +++ b/Assets/GameMain/Scripts/UI/Combat/Controller/CombatFinishFormController.cs @@ -18,7 +18,8 @@ namespace GeometryTD.UI public string Title; public string TypeText; public string Description; - public string[] TagTexts; + public TagType[] Tags; + public TagRuntimeData[] TagRuntimes; } protected override UIFormType UIFormTypeId => UIFormType.CombatFinishForm; @@ -138,7 +139,8 @@ namespace GeometryTD.UI tower.Name, "Tower", ItemDescUtility.BuildTowerDesc(tower, muzzleMap, bearingMap, baseMap) ?? string.Empty, - TagDisplayUtility.BuildTowerTagTexts(tower.Stats)); + tower.Stats?.Tags, + tower.Stats?.TagRuntimes); } } @@ -166,7 +168,8 @@ namespace GeometryTD.UI item.Name, BuildComponentTypeText(item.SlotType), ItemDescUtility.BuildMuzzleDesc(item) ?? string.Empty, - TagDisplayUtility.BuildTagTexts(item.Tags)); + item.Tags, + null); } } @@ -194,7 +197,8 @@ namespace GeometryTD.UI item.Name, BuildComponentTypeText(item.SlotType), ItemDescUtility.BuildBearingDesc(item) ?? string.Empty, - TagDisplayUtility.BuildTagTexts(item.Tags)); + item.Tags, + null); } } @@ -222,7 +226,8 @@ namespace GeometryTD.UI item.Name, BuildComponentTypeText(item.SlotType), ItemDescUtility.BuildBaseDesc(item) ?? string.Empty, - TagDisplayUtility.BuildTagTexts(item.Tags)); + item.Tags, + null); } } @@ -261,7 +266,13 @@ namespace GeometryTD.UI return Color.white; } - private void AddItemDescSeed(long itemId, string title, string typeText, string description, string[] tagTexts) + private void AddItemDescSeed( + long itemId, + string title, + string typeText, + string description, + TagType[] tags, + TagRuntimeData[] tagRuntimes) { if (itemId <= 0) { @@ -273,13 +284,19 @@ namespace GeometryTD.UI Title = string.IsNullOrWhiteSpace(title) ? $"Item {itemId}" : title, TypeText = typeText ?? string.Empty, Description = description ?? string.Empty, - TagTexts = CloneTagTexts(tagTexts) + Tags = CloneTags(tags), + TagRuntimes = CloneTagRuntimes(tagRuntimes) }; } - private static string[] CloneTagTexts(string[] tagTexts) + private static TagType[] CloneTags(TagType[] tags) { - return tagTexts != null ? (string[])tagTexts.Clone() : System.Array.Empty(); + return tags != null ? (TagType[])tags.Clone() : System.Array.Empty(); + } + + private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] tagRuntimes) + { + return InventoryCloneUtility.CloneTagRuntimes(tagRuntimes); } private static string BuildComponentTypeText(TowerCompSlotType slotType) @@ -374,7 +391,8 @@ namespace GeometryTD.UI Description = seed.Description ?? string.Empty, Price = 0, TargetPos = args.TargetPos, - TagTexts = CloneTagTexts(seed.TagTexts) + Tags = CloneTags(seed.Tags), + TagRuntimes = CloneTagRuntimes(seed.TagRuntimes) }); } @@ -402,6 +420,3 @@ namespace GeometryTD.UI } } } - - - diff --git a/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.ContextBuilder.cs b/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.ContextBuilder.cs index 2d01dbd..e0dd626 100644 --- a/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.ContextBuilder.cs +++ b/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.ContextBuilder.cs @@ -43,7 +43,8 @@ namespace GeometryTD.UI tower.Name, "Tower", ItemDescUtility.BuildTowerDesc(tower, muzzleMap, bearingMap, baseMap) ?? string.Empty, - TagDisplayUtility.BuildTowerTagTexts(tower.Stats)); + tower.Stats?.Tags, + tower.Stats?.TagRuntimes); } } @@ -71,7 +72,8 @@ namespace GeometryTD.UI item.Name, BuildComponentTypeText(item.SlotType), ItemDescUtility.BuildMuzzleDesc(item) ?? string.Empty, - TagDisplayUtility.BuildTagTexts(item.Tags)); + item.Tags, + null); } } @@ -99,7 +101,8 @@ namespace GeometryTD.UI item.Name, BuildComponentTypeText(item.SlotType), ItemDescUtility.BuildBearingDesc(item) ?? string.Empty, - TagDisplayUtility.BuildTagTexts(item.Tags)); + item.Tags, + null); } } @@ -127,7 +130,8 @@ namespace GeometryTD.UI item.Name, BuildComponentTypeText(item.SlotType), ItemDescUtility.BuildBaseDesc(item) ?? string.Empty, - TagDisplayUtility.BuildTagTexts(item.Tags)); + item.Tags, + null); } } @@ -169,7 +173,13 @@ namespace GeometryTD.UI _compAreaTowerIds.Add(itemContext.InstanceId); } - private void AddItemDescSeed(long itemId, string title, string typeText, string description, string[] tagTexts) + private void AddItemDescSeed( + long itemId, + string title, + string typeText, + string description, + TagType[] tags, + TagRuntimeData[] tagRuntimes) { if (itemId <= 0) { @@ -181,7 +191,8 @@ namespace GeometryTD.UI Title = string.IsNullOrWhiteSpace(title) ? $"Item {itemId}" : title, TypeText = typeText ?? string.Empty, Description = description ?? string.Empty, - TagTexts = tagTexts != null ? (string[])tagTexts.Clone() : Array.Empty() + Tags = CloneTags(tags), + TagRuntimes = CloneTagRuntimes(tagRuntimes) }; } diff --git a/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.cs b/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.cs index ad12763..3d1441a 100644 --- a/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.cs +++ b/Assets/GameMain/Scripts/UI/Game/Controller/RepoFormController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using GeometryTD.CustomEvent; +using GeometryTD.CustomUtility; using GeometryTD.Definition; using GameFramework.Event; using UnityEngine; @@ -22,7 +23,8 @@ namespace GeometryTD.UI public string Title; public string TypeText; public string Description; - public string[] TagTexts; + public TagType[] Tags; + public TagRuntimeData[] TagRuntimes; } protected override UIFormType UIFormTypeId => UIFormType.RepoForm; @@ -151,10 +153,21 @@ namespace GeometryTD.UI Description = seed.Description ?? string.Empty, Price = 0, TargetPos = args.TargetPos, - TagTexts = CloneTagTexts(seed.TagTexts) + Tags = CloneTags(seed.Tags), + TagRuntimes = CloneTagRuntimes(seed.TagRuntimes) }); } + private static TagType[] CloneTags(TagType[] tags) + { + return tags != null ? (TagType[])tags.Clone() : System.Array.Empty(); + } + + private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] tagRuntimes) + { + return InventoryCloneUtility.CloneTagRuntimes(tagRuntimes); + } + private void OnRepoItemDragEnded(object sender, GameEventArgs e) { if (!IsEventFromCurrentForm(sender)) diff --git a/Assets/GameMain/Scripts/UI/General/Controller/ItemDescFormController.cs b/Assets/GameMain/Scripts/UI/General/Controller/ItemDescFormController.cs index 696cdca..b6478d9 100644 --- a/Assets/GameMain/Scripts/UI/General/Controller/ItemDescFormController.cs +++ b/Assets/GameMain/Scripts/UI/General/Controller/ItemDescFormController.cs @@ -32,20 +32,39 @@ namespace GeometryTD.UI { Title = rawData.Title, TypeText = rawData.TypeText, - Description = rawData.Description, + Description = BuildDescription(rawData), Price = rawData.Price, TargetPos = rawData.TargetPos, Tags = BuildTags(rawData) }; } + private static string BuildDescription(ItemDescFormRawData rawData) + { + string baseDescription = rawData?.Description ?? string.Empty; + string tagDescription = rawData?.TagRuntimes != null && rawData.TagRuntimes.Length > 0 + ? TagDisplayUtility.BuildTagDescriptionText(rawData.TagRuntimes) + : TagDisplayUtility.BuildTagDescriptionText(rawData?.Tags); + if (string.IsNullOrWhiteSpace(tagDescription)) + { + return baseDescription; + } + + if (string.IsNullOrWhiteSpace(baseDescription)) + { + return tagDescription; + } + + return $"{baseDescription}\n{tagDescription}"; + } + private static TagItemContext[] BuildTags(ItemDescFormRawData rawData) { - string[] tagTexts = rawData?.TagTexts; - if ((tagTexts == null || tagTexts.Length <= 0) && rawData?.Tags != null) - { - tagTexts = TagDisplayUtility.BuildTagTexts(rawData.Tags); - } + string[] tagTexts = rawData?.TagRuntimes != null && rawData.TagRuntimes.Length > 0 + ? TagDisplayUtility.BuildTagTexts(rawData.TagRuntimes) + : rawData?.Tags != null + ? TagDisplayUtility.BuildTagTexts(rawData.Tags) + : null; if (tagTexts == null || tagTexts.Length <= 0) { diff --git a/Assets/GameMain/Scripts/UI/General/RawData/ItemDescFormRawData.cs b/Assets/GameMain/Scripts/UI/General/RawData/ItemDescFormRawData.cs index 707e63e..5ba738d 100644 --- a/Assets/GameMain/Scripts/UI/General/RawData/ItemDescFormRawData.cs +++ b/Assets/GameMain/Scripts/UI/General/RawData/ItemDescFormRawData.cs @@ -11,6 +11,6 @@ namespace GeometryTD.UI public int Price; public Vector3 TargetPos; public TagType[] Tags; - public string[] TagTexts; + public TagRuntimeData[] TagRuntimes; } } diff --git a/Assets/GameMain/Scripts/Utility/TagDisplayUtility.cs b/Assets/GameMain/Scripts/Utility/TagDisplayUtility.cs index 093efca..a627e25 100644 --- a/Assets/GameMain/Scripts/Utility/TagDisplayUtility.cs +++ b/Assets/GameMain/Scripts/Utility/TagDisplayUtility.cs @@ -92,5 +92,84 @@ namespace GeometryTD.CustomUtility return BuildTagTexts(towerStats.Tags); } + + public static string BuildTagDescriptionText(IReadOnlyList tags) + { + if (tags == null || tags.Count <= 0) + { + return string.Empty; + } + + List results = new List(tags.Count); + for (int i = 0; i < tags.Count; i++) + { + TagType tagType = tags[i]; + if (tagType == TagType.None) + { + continue; + } + + string tagName = ResolveTagName(tagType); + string tagDescription = ResolveTagDescription(tagType); + if (string.IsNullOrWhiteSpace(tagDescription)) + { + continue; + } + + results.Add(string.IsNullOrWhiteSpace(tagName) ? tagDescription : $"{tagName}: {tagDescription}"); + } + + return string.Join("\n", results); + } + + public static string BuildTagDescriptionText(IReadOnlyList tagRuntimes) + { + if (tagRuntimes == null || tagRuntimes.Count <= 0) + { + return string.Empty; + } + + List results = new List(tagRuntimes.Count); + for (int i = 0; i < tagRuntimes.Count; i++) + { + TagRuntimeData runtime = tagRuntimes[i]; + if (runtime == null || runtime.TagType == TagType.None || runtime.TotalStack <= 0) + { + continue; + } + + string tagName = ResolveTagName(runtime.TagType); + if (runtime.TotalStack > 1 && !string.IsNullOrWhiteSpace(tagName)) + { + tagName = $"{tagName} x{runtime.TotalStack}"; + } + + string tagDescription = ResolveTagDescription(runtime.TagType); + if (string.IsNullOrWhiteSpace(tagDescription)) + { + continue; + } + + results.Add(string.IsNullOrWhiteSpace(tagName) ? tagDescription : $"{tagName}: {tagDescription}"); + } + + return string.Join("\n", results); + } + + public static string ResolveTagDescription(TagType tagType) + { + if (tagType == TagType.None) + { + return string.Empty; + } + + if (TagConfigRegistry.TryGetDefinition(tagType, out TagDefinitionData definition) && + !string.IsNullOrWhiteSpace(definition.Description)) + { + return definition.Description; + } + + return string.Empty; + } } } diff --git a/Assets/Tests/EditMode/TagCombatRuntimeTests.cs b/Assets/Tests/EditMode/TagCombatRuntimeTests.cs index 042dba0..7c13689 100644 --- a/Assets/Tests/EditMode/TagCombatRuntimeTests.cs +++ b/Assets/Tests/EditMode/TagCombatRuntimeTests.cs @@ -1,7 +1,9 @@ using Components; +using GeometryTD.DataTable; using GeometryTD.Definition; using GeometryTD.Entity; using GeometryTD.Entity.EntityData; +using GeometryTD.CustomUtility; using NUnit.Framework; using UnityEngine; @@ -47,6 +49,47 @@ namespace GeometryTD.Tests.EditMode Assert.That(lowHealthHit.FinalDamage, Is.EqualTo(150)); Assert.That(highHealthHit.FinalDamage, Is.EqualTo(100)); } + + [Test] + public void ResolveBeforeHit_Shatter_OnlyTriggersOnSlowedTarget() + { + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + AttackPropertyType = AttackPropertyType.Physics, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Shatter, TotalStack = 2 } + } + }; + + HitContext slowedHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 100, 100, true); + HitContext normalHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 100, 100, false); + + Assert.That(slowedHit.FinalDamage, Is.EqualTo(150)); + Assert.That(normalHit.FinalDamage, Is.EqualTo(100)); + } + + [Test] + public void ResolveBeforeHit_Shatter_CanStackWithOtherNumericTags() + { + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + AttackPropertyType = AttackPropertyType.Physics, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Crit, TotalStack = 1 }, + new TagRuntimeData { TagType = TagType.Execution, TotalStack = 1 }, + new TagRuntimeData { TagType = TagType.Shatter, TotalStack = 1 } + } + }; + + HitContext hitContext = TagEffectResolver.ResolveBeforeHit(attackPayload, 30, 100, true, 0.05f); + + Assert.That(hitContext.IsCriticalHit, Is.True); + Assert.That(hitContext.FinalDamage, Is.EqualTo(281)); + } } public sealed class EnemyTagStatusRuntimeTests @@ -124,6 +167,101 @@ namespace GeometryTD.Tests.EditMode Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(1f).Within(0.001f)); } + [Test] + public void ApplyAfterHit_FireWithInferno_IncreasesBurnDamageAndDuration() + { + EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime(); + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Fire, TotalStack = 1 }, + new TagRuntimeData { TagType = TagType.Inferno, TotalStack = 1 } + } + }; + int totalDamage = 0; + + TagEffectResolver.ApplyAfterHit(attackPayload, runtime); + Assert.That(runtime.HasStatus(TagType.Fire), Is.True); + Assert.That(runtime.HasStatus(TagType.Inferno), Is.False); + + runtime.Tick(1f, damage => totalDamage += damage); + runtime.Tick(1f, damage => totalDamage += damage); + runtime.Tick(1f, damage => totalDamage += damage); + runtime.Tick(1f, damage => totalDamage += damage); + runtime.Tick(1f, damage => totalDamage += damage); + + Assert.That(totalDamage, Is.EqualTo(200)); + Assert.That(runtime.HasStatus(TagType.Fire), Is.False); + } + + [Test] + public void ApplyAfterHit_InfernoWithoutFire_DoesNotCreateStatus() + { + EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime(); + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Inferno, TotalStack = 1 } + } + }; + + TagEffectResolver.ApplyAfterHit(attackPayload, runtime); + + Assert.That(runtime.HasStatus(TagType.Fire), Is.False); + Assert.That(runtime.HasStatus(TagType.Inferno), Is.False); + } + + [Test] + public void ApplyAfterHit_IceWithAbsoluteZero_IncreasesSlowStrengthAndDuration() + { + EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime(); + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Ice, TotalStack = 2 }, + new TagRuntimeData { TagType = TagType.AbsoluteZero, TotalStack = 1 } + } + }; + + TagEffectResolver.ApplyAfterHit(attackPayload, runtime); + + Assert.That(runtime.HasStatus(TagType.Ice), Is.True); + Assert.That(runtime.HasStatus(TagType.AbsoluteZero), Is.False); + Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(0.5f).Within(0.001f)); + + runtime.Tick(2.5f, null); + Assert.That(runtime.HasStatus(TagType.Ice), Is.True); + + runtime.Tick(0.5f, null); + Assert.That(runtime.HasStatus(TagType.Ice), Is.False); + Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(1f).Within(0.001f)); + } + + [Test] + public void ApplyAfterHit_AbsoluteZeroWithoutIce_DoesNotCreateStatus() + { + EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime(); + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.AbsoluteZero, TotalStack = 1 } + } + }; + + TagEffectResolver.ApplyAfterHit(attackPayload, runtime); + + Assert.That(runtime.HasStatus(TagType.Ice), Is.False); + Assert.That(runtime.HasStatus(TagType.AbsoluteZero), Is.False); + } + [Test] public void Tick_OnlyUpdatesActivatedStatusTags() { @@ -177,21 +315,84 @@ namespace GeometryTD.Tests.EditMode } [Test] - public void Definitions_Keep_Unimplemented_Tags_As_Explicit_NoOpPlaceholders() + public void Definitions_Mark_FirstBatch_SevenTags_AsImplemented() { Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Shatter, out TagDefinitionData shatter), Is.True); Assert.That(shatter.Config, Is.TypeOf()); - Assert.That(((ShatterTagConfig)shatter.Config).IsImplemented, Is.False); + Assert.That(((ShatterTagConfig)shatter.Config).IsImplemented, Is.True); Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Inferno, out TagDefinitionData inferno), Is.True); Assert.That(inferno.Config, Is.TypeOf()); - Assert.That(((InfernoTagConfig)inferno.Config).IsImplemented, Is.False); + Assert.That(((InfernoTagConfig)inferno.Config).IsImplemented, Is.True); + + Assert.That(TagConfigRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinitionData absoluteZero), Is.True); + Assert.That(absoluteZero.Config, Is.TypeOf()); + Assert.That(((AbsoluteZeroTagConfig)absoluteZero.Config).IsImplemented, Is.True); Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Pierce, out TagDefinitionData pierce), Is.True); Assert.That(pierce.Config, Is.TypeOf()); Assert.That(((PierceTagConfig)pierce.Config).IsImplemented, Is.False); } + [Test] + public void LoadFromRows_Overrides_TriggerPhase_And_ConfigValues() + { + DRTagConfig fireRow = new DRTagConfig(); + bool parsed = fireRow.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t火焰测试描述\t{\"BurnDurationSeconds\":4,\"BurnDamagePerSecondPerStack\":25,\"MaxEffectiveStack\":3}", null); + Assert.That(parsed, Is.True); + + TagConfigRegistry.LoadFromRows(new[] { fireRow }); + + Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Fire, out TagDefinitionData fire), Is.True); + Assert.That(fire.TriggerPhase, Is.EqualTo(TagTriggerPhase.OnAfterHit)); + Assert.That(fire.Description, Is.EqualTo("火焰测试描述")); + Assert.That(fire.Config, Is.TypeOf()); + + FireTagConfig config = (FireTagConfig)fire.Config; + Assert.That(config.BurnDurationSeconds, Is.EqualTo(4f).Within(0.001f)); + Assert.That(config.BurnDamagePerSecondPerStack, Is.EqualTo(25f).Within(0.001f)); + Assert.That(config.MaxEffectiveStack, Is.EqualTo(3)); + + TagConfigRegistry.ResetToDefaults(); + } + + [Test] + public void BuildTagDescriptionText_Uses_Registry_Descriptions() + { + DRTagConfig fireRow = new DRTagConfig(); + DRTagConfig shatterRow = new DRTagConfig(); + Assert.That(fireRow.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t持续燃烧\t{\"BurnDurationSeconds\":3,\"BurnDamagePerSecondPerStack\":20,\"MaxEffectiveStack\":5}", null), Is.True); + Assert.That(shatterRow.ParseDataRow("\t7\t控制\tShatter\tOnBeforeHit\t对减速目标增伤\t{\"RequiresSlowedTarget\":true,\"DamageBonusPerStack\":0.25}", null), Is.True); + + TagConfigRegistry.LoadFromRows(new[] { fireRow, shatterRow }); + + string description = TagDisplayUtility.BuildTagDescriptionText(new[] { TagType.Fire, TagType.Shatter }); + + Assert.That(description, Does.Contain("持续燃烧")); + Assert.That(description, Does.Contain("对减速目标增伤")); + + TagConfigRegistry.ResetToDefaults(); + } + + [Test] + public void BuildTagDescriptionText_FromTagRuntimes_Preserves_StackText() + { + DRTagConfig iceRow = new DRTagConfig(); + Assert.That(iceRow.ParseDataRow("\t5\t控制\tIce\tOnAfterHit\t命中附加减速\t{\"SlowDurationSeconds\":2,\"SlowRatioPerStack\":0.2,\"MinMoveSpeedMultiplier\":0.4}", null), Is.True); + + TagConfigRegistry.LoadFromRows(new[] { iceRow }); + + string description = TagDisplayUtility.BuildTagDescriptionText(new[] + { + new TagRuntimeData { TagType = TagType.Ice, TotalStack = 2 } + }); + + Assert.That(description, Does.Contain("Ice x2")); + Assert.That(description, Does.Contain("命中附加减速")); + + TagConfigRegistry.ResetToDefaults(); + } + [Test] public void AttackShape_Placeholders_Are_Routable_Without_Runtime_Effect() { diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index 8d56d4e..1690080 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -130,8 +130,8 @@ | [x] | S4-03 | 先固化 Tag 系统设计与首发范围 | `docs/TagSystemDesign.md`
`docs/CodeX-TODO.md` | Tag 的来源、汇总、生效与首发集合口径固定 | | [x] | S4-04 | 实现组件实例 Tag 的统一生成入口 | `Assets/GameMain/Scripts/Definition/`
`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/` | 掉落、商店、初始种子、事件奖励共用同一生成结果 | | [x] | S4-05 | 实现组塔后的 Tag 汇总与展示入口 | `Assets/GameMain/Scripts/Definition/`
`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`
`Assets/GameMain/Scripts/UI/` | 组件 Tag 可汇总为塔级结果,且 UI 展示口径一致 | -| [ ] | S4-06 | 实现首批基础 Tag 的战斗生效 | `Assets/GameMain/Scripts/Entity/`
`Assets/GameMain/Scripts/Components/`
`Assets/GameMain/Scripts/Definition/` | 首发 6~8 个基础 Tag 至少完成一批可验证效果,且战斗入口与状态运行时结构可继续扩展 | -| [ ] | S4-07 | 补齐 Tag 规则与数据表的映射关系 | `Assets/GameMain/Scripts/DataTable/`
`Assets/GameMain/Scripts/Definition/` | 表字段不是只存在而未被消费,Tag 参数可配置可解释 | +| [x] | S4-06 | 实现首批基础 Tag 的战斗生效 | `Assets/GameMain/Scripts/Entity/`
`Assets/GameMain/Scripts/Components/`
`Assets/GameMain/Scripts/Definition/` | 首发 6~8 个基础 Tag 至少完成一批可验证效果,且战斗入口与状态运行时结构可继续扩展 | +| [~] | S4-07 | 补齐 Tag 规则与数据表的映射关系 | `Assets/GameMain/Scripts/DataTable/`
`Assets/GameMain/Scripts/Definition/` | 表字段不是只存在而未被消费,Tag 参数可配置可解释 | > 2026-03-09 更新:`InventoryRarityRuleService` 已落地;塔品质计算与组件品质归一化已统一收口,`PlayerInventoryTowerAssemblyService`、`ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 已接入同一规则入口。你已确认 Unity Test Runner 中 `Assets/Tests/EditMode` 全部通过,其中包含新增的 `InventoryRarityRuleServiceTests`。 > @@ -143,9 +143,11 @@ > > 2026-03-10 更新:`S4-06` 已完成第一段落地。战斗链现在已从旧的 `damage + AttackPropertyType` 入口提升为携带 `TagRuntimeData[]` 的命中载荷;`AttackPayload`、`HitContext`、`TagEffectResolver`、`EnemyTagStatusRuntime` 已接入主命中链。当前已实际生效的基础 Tag 为 `Fire`、`Ice`、`Crit`、`Execution`:其中 `Fire` 已改为独立 DOT 公式,并按 `EnemyEntity` 每帧提供的 `deltaTime` 连续结算;`Ice` 已有独立减速状态;`Crit` 与 `Execution` 已进入命中前数值修正。状态类 Tag 已按“每个 Tag 各自配置文件、状态文件、效果文件”的方式拆开,`EnemyTagStatusRuntime` 只负责按激活 Tag 动态 Tick,不再持有具体 Tag 字段。 > -> 2026-03-10 更新:当前 `S4-06` 仍未整体完成。`Shatter`、`Inferno`、`AbsoluteZero` 仍处于“已注册但 no-op 占位”的状态;`BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 仍只有分类与占位路由,没有实际战斗效果。因此当前仓库状态应视为“首批 4 个基础 Tag 已进入战斗,首发 7 个 Tag 尚未收满”,而不是 `S4-06` 全量完成。 +> 2026-03-10 更新:`S4-06` 已补齐首发剩余 3 个 Tag。`Shatter` 已接入命中前数值修正链,并按“目标已减速”口径增伤;`Inferno` 与 `AbsoluteZero` 已按首发方案作为 `Fire` / `Ice` 的强化 Tag 落地,分别增强 DOT 时长/伤害与减速时长/强度;对应 EditMode 测试已同步补齐。`BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 仍保持分类与占位路由,没有实际战斗效果,因此仍属于后续扩展,不计入 `S4-06` 当前完成标准。 > > 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`(如权重、生成规则等)全部收口。 ### S4-01 边界结论 @@ -177,10 +179,11 @@ ### S4 当前进度结论(2026-03-10) - `S4-02 ~ S4-05` 已完成,品质、组件实例 Tag 生成、塔级 `TagRuntimeData` 汇总、以及 UI 展示口径都已收口。 -- `S4-06` 已完成第一阶段:战斗链已支持 Tag 透传与统一结算,`Fire`、`Ice`、`Crit`、`Execution` 已有可验证效果,状态类 Tag 的内部结构也已从集中字段改为注册式运行时。 -- 当前 `S4-06` 的未完成部分集中在首发剩余 3 个 Tag:`Shatter`、`Inferno`、`AbsoluteZero`。它们虽然已进入 `TagType`、配置类、注册表和占位效果类,但还没有实际战斗语义。 +- `S4-06` 已完成:战斗链已支持 Tag 透传与统一结算,正式首发 7 个 Tag `Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero` 均已有可验证效果。 +- `Shatter` 已接入命中前数值修正链;`Inferno`、`AbsoluteZero` 已按“强化现有 `Fire` / `Ice` 状态”的首发口径落地,状态类 Tag 的内部结构继续保持注册式运行时。 - `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 已进入分类与配置骨架,但仍属于后续扩展,不计入当前 `S4` 的完成标准。 -- `S4-07` 仍未开始正式实现。当前仓库只有“代码内注册表配置”,还没有把 `TagRule` 设计回写成 DataTable / DataRow / 运行时消费链。 +- `S4-07` 已进入第一阶段实现。当前仓库已具备 `TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 的消费闭环,首发 7 个 Tag 的触发阶段、描述与核心参数已可由表覆盖。 +- `S4-07` 仍未完成最终收口。当前采用的是 `TagConfig` 表驱动,而不是文档原方案里的完整 `TagRule`;权重、生成规则等字段仍未全部映射回 DataTable / DataRow / 运行时消费链。 ### S4-06 当前代码状态 @@ -193,16 +196,14 @@ - `EnemyTagStatusRuntime` 已不再硬编码 `burn/slow` 字段,而是按激活的状态类 Tag 动态调度 Tick。 - `Fire` 已改为独立 DOT 公式:当前使用独立 `BurnDamagePerSecondPerStack`,并按敌人实体每帧提供的 `deltaTime` 连续结算,不再依赖命中伤害或内部 `TickInterval`。 - `Ice` 仍是独立减速状态,移速倍率通过状态运行时聚合得到。 -- `Crit`、`Execution` 已通过数值修正链路生效;`Shatter` 仍仅有占位配置与入口,没有实际增伤逻辑。 +- `Crit`、`Execution`、`Shatter` 已通过数值修正链路生效;`Shatter` 当前按“目标已减速时增伤”收口。 +- `Inferno` 与 `AbsoluteZero` 已不再是独立状态效果,而是分别在 `Fire` / `Ice` 生效时读取同次命中的强化 Tag 层数,增强 DOT 时长/伤害与减速时长/强度。 ### S4 后续执行计划 -1. 先补完 `S4-06` 的首发缺口,只做正式首发集合剩余的 `Shatter`、`Inferno`、`AbsoluteZero`,不提前展开传播、爆炸、穿透、多命中体系。 -2. `Shatter` 优先接入现有命中前数值修正链,直接消费“目标已减速”这一当前已存在的状态查询能力。 -3. `Inferno` 与 `AbsoluteZero` 优先作为对已有 `Fire` / `Ice` 的强化 Tag 落地,不额外引入第二套状态系统;要求仍沿用当前“单 Tag 配置 + 单 Tag 效果 + 注册式状态运行时”的结构。 -4. 首发 7 个 Tag 全部进入战斗并补齐对应 EditMode 测试后,再将 `S4-06` 标记为完成。 -5. 之后进入 `S4-07`:新增并消费 `TagRule` 表或等价 DataTable 映射,把当前代码内的 Tag 参数逐步迁移为可配置数据,而不是继续堆在 `TagConfigRegistry` 默认值里。 -6. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准,不要求一步到位把全部 12 个 Tag 都配置化。 +1. 继续推进 `S4-07`:在现有 `TagConfig` 表驱动基础上,决定是否补成文档中的完整 `TagRule`,或明确将 `TagConfig` 作为当前阶段的等价收口方案。 +2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的参数和说明配置化,后续重点转向剩余规则字段是否也要配置化。 +3. `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 继续留在后续扩展阶段,不因为 `S4-06` 完成而提前进入当前迭代。 ## 阶段 S5 - 收口耐久规则 @@ -227,7 +228,7 @@ 1. 先做 `S1`,把主流程真正收成一条稳定可验收的链。 2. `S2` 已完成口径对齐,`NodeMapForm` 不再作为 M1 阻塞项。 3. 接下来优先做 `S3`,补齐出战合法性,解决 `P0-10`。 -4. 再做 `S4`,统一品质 / Tag 的规则口径,解决 `P0-11`。其中当前优先级已经收口为:先补完 `S4-06` 的首发 7 Tag,再做 `S4-07` 的数据表映射。 +4. 再做 `S4`,统一品质 / Tag 的规则口径,解决 `P0-11`。其中当前优先级已经收口为:`S4-06` 已完成,下一步转入 `S4-07` 的数据表映射。 5. 最后做 `S5`,决定耐久是完整收口还是同步缩范围,解决 `P0-12`。 6. 全部完成后做 `S6`,补测试并同步文档状态。 @@ -235,9 +236,8 @@ 1. `S1` 与 `S2` 已完成口径收口,可直接进入规则侧收尾 2. `S3-01 ~ S3-04` 已完成,当前可转入 `S4` -3. 当前先继续完成 `S4-06`,补齐 `Shatter`、`Inferno`、`AbsoluteZero` -4. 然后进入 `S4-07`,把 `TagRule` / DataTable 映射真正接进运行时 -5. 最后补 `S6-01 ~ S6-04` +3. 当前转入 `S4-07`,把 `TagRule` / DataTable 映射真正接进运行时 +4. 然后补 `S6-01 ~ S6-04` ## 备注 diff --git a/数据表/Tag.xlsx b/数据表/Tag.xlsx index eed5152..a6607b7 100644 Binary files a/数据表/Tag.xlsx and b/数据表/Tag.xlsx differ