From 9de2e5026268b9b6d5d39508cb48675e65889d38 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Wed, 11 Mar 2026 10:18:14 +0800 Subject: [PATCH] S4-07 process 1 --- .gitignore | 3 +- Assets/GameMain/DataTables/Tag.txt | 3 +- Assets/GameMain/DataTables/TagConfig.txt | 51 ++++ Assets/GameMain/DataTables/TagConfig.txt.meta | 7 + Assets/GameMain/DataTables/UIForm.txt | 2 +- .../GameMain/Scripts/DataTable/DRTagConfig.cs | 41 +++ .../Scripts/DataTable/DRTagConfig.cs.meta | 3 + .../Definition/DataStruct/TowerItemData.cs | 20 +- .../Definition/DataStruct/TowerStatsData.cs | 22 ++ .../DataStruct/TowerStatsData.cs.meta | 3 + .../Tag/EnemyStatusTagEffect/FireTagEffect.cs | 46 +++- .../Tag/EnemyStatusTagEffect/IceTagEffect.cs | 45 +++- .../Tag/TagConfig/AbsoluteZeroTagConfig.cs | 6 +- .../Definition/Tag/TagConfig/FireTagConfig.cs | 3 +- .../Tag/TagConfig/InfernoTagConfig.cs | 4 +- .../Tag/TagConfig/PierceTagConfig.cs | 2 +- .../Tag/TagConfig/ShatterTagConfig.cs | 4 +- .../Definition/Tag/TagConfigRegistry.cs | 247 +++++++++++------- .../Definition/Tag/TagDefinitionData.cs | 1 + .../NumericTagEffectHandler.cs | 16 +- .../Definition/TowerTagAggregationService.cs | 14 +- .../Scripts/Entity/EntityData/TowerData.cs | 10 +- .../Procedure/Base/ProcedurePreload.cs | 10 + .../Controller/CombatFinishFormController.cs | 41 ++- .../RepoFormController.ContextBuilder.cs | 23 +- .../UI/Game/Controller/RepoFormController.cs | 17 +- .../Controller/ItemDescFormController.cs | 31 ++- .../UI/General/RawData/ItemDescFormRawData.cs | 2 +- .../Scripts/Utility/TagDisplayUtility.cs | 79 ++++++ .../Tests/EditMode/TagCombatRuntimeTests.cs | 207 ++++++++++++++- docs/CodeX-TODO.md | 34 +-- 数据表/Tag.xlsx | Bin 10756 -> 14576 bytes 32 files changed, 803 insertions(+), 194 deletions(-) create mode 100644 Assets/GameMain/DataTables/TagConfig.txt create mode 100644 Assets/GameMain/DataTables/TagConfig.txt.meta create mode 100644 Assets/GameMain/Scripts/DataTable/DRTagConfig.cs create mode 100644 Assets/GameMain/Scripts/DataTable/DRTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/DataStruct/TowerStatsData.cs create mode 100644 Assets/GameMain/Scripts/Definition/DataStruct/TowerStatsData.cs.meta 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 eed5152f00b70a6b745cad12b05adc007dee7200..a6607b72af2cb00668738ffaf7f1b4a8727fa6da 100644 GIT binary patch delta 10309 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