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