S4-07 process 1

This commit is contained in:
SepComet
2026-03-11 10:18:14 +08:00
parent 52f9e212b9
commit 9de2e50262
32 changed files with 803 additions and 194 deletions
+1 -2
View File
@@ -1,5 +1,4 @@
# Tag表
# Id Name MinRarity
# Id 列1 Name MinRarity
# int string RarityType
# Tag编号 策划备注 Tag名 获取最低稀有度
1 元素 Fire White
+51
View File
@@ -0,0 +1,51 @@
# Id 列1 TagType Weight TriggerPhase Description Param
# int TagType int TagTriggerPhase string string
# Tag配置编号 策划备注 所属Tag类型 权重 触发阶段 描述 参数Json
1 元素 Fire 20 OnAfterHit 持续对敌人造成<color=red>火</color>伤害 {\
"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\
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 237e82d08a4faa64da2401808bc652fc
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+1 -1
View File
@@ -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
@@ -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<TagType>.Get(columnStrings[index++]);
TriggerPhase = EnumUtility<TagTriggerPhase>.Get(columnStrings[index++]);
Description = columnStrings[index++];
ParamJson = columnStrings[index++];
return true;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3cd426c0ec84c0bbfd9918d768464f9
timeCreated: 1773139200
@@ -4,24 +4,6 @@ using UnityEngine;
namespace GeometryTD.Definition
{
/// <summary>
/// 防御塔独立属性快照。
/// 注意:这里是塔实例的独立值,不通过组件引用实时计算。
/// </summary>
[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; }
}
/// <summary>
/// 背包内防御塔实例数据。
/// </summary>
@@ -69,4 +51,4 @@ namespace GeometryTD.Definition
[JsonIgnore] public string ComposedIconKey { get; set; }
}
}
}
@@ -0,0 +1,22 @@
using System;
namespace GeometryTD.Definition
{
/// <summary>
/// 防御塔独立属性快照。
/// 注意:这里是塔实例的独立值,不通过组件引用实时计算。
/// </summary>
[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; }
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 33934c152fda4ff4827aabfbf6f65856
timeCreated: 1773194446
@@ -21,15 +21,19 @@ namespace GeometryTD.Definition
}
FireTagState state = runtime.GetOrCreateState<FireTagState>(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;
}
}
}
}
@@ -24,9 +24,12 @@ namespace GeometryTD.Definition
}
IceTagState state = runtime.GetOrCreateState<IceTagState>(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<IceTagState>(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;
}
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -9,6 +9,6 @@ namespace GeometryTD.Definition
{
}
public int ExtraPierceCount { get; set; } = 0;
public int ExtraPierceCount { get; set; } = 2;
}
}
@@ -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;
}
}
}
@@ -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<TagType, TagDefinitionData> DefinitionsByTag =
new Dictionary<TagType, TagDefinitionData>
{
[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<TagType, TagDefinitionData> DefinitionsByTag = CreateDefaultDefinitions();
public static IReadOnlyDictionary<TagType, TagDefinitionData> Definitions => DefinitionsByTag;
public static void ResetToDefaults()
{
DefinitionsByTag.Clear();
foreach (KeyValuePair<TagType, TagDefinitionData> pair in CreateDefaultDefinitions())
{
DefinitionsByTag.Add(pair.Key, pair.Value);
}
}
public static void LoadFromRows(IEnumerable<DRTagConfig> 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<TagType, TagDefinitionData> CreateDefaultDefinitions()
{
return new Dictionary<TagType, TagDefinitionData>
{
[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<float>();
}
private static int ReadInt(JObject param, string key, int defaultValue)
{
JToken token = param[key];
return token == null ? defaultValue : token.Value<int>();
}
private static bool ReadBool(JObject param, string key, bool defaultValue)
{
JToken token = param[key];
return token == null ? defaultValue : token.Value<bool>();
}
}
}
@@ -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; }
}
}
@@ -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));
}
}
}
@@ -10,17 +10,15 @@ namespace GeometryTD.Definition
Dictionary<TagType, int> stackByTag = new Dictionary<TagType, int>();
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<TagRuntimeData> runtimes = new List<TagRuntimeData>(stackByTag.Count);
foreach (KeyValuePair<TagType, int> pair in stackByTag)
foreach (var pair in stackByTag)
{
runtimes.Add(new TagRuntimeData
{
@@ -65,9 +63,8 @@ namespace GeometryTD.Definition
}
HashSet<TagType> uniqueTags = new HashSet<TagType>();
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<TagType> tags = new List<TagType>(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;
@@ -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;
}
}
}
}
@@ -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<DRTagConfig>();
if (tagConfigTable != null)
{
TagConfigRegistry.LoadFromRows(tagConfigTable.GetAllDataRows());
}
}
}
private void OnLoadDataTableFailure(object sender, GameEventArgs e)
@@ -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<string>();
return tags != null ? (TagType[])tags.Clone() : System.Array.Empty<TagType>();
}
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
}
}
}
@@ -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<string>()
Tags = CloneTags(tags),
TagRuntimes = CloneTagRuntimes(tagRuntimes)
};
}
@@ -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<TagType>();
}
private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] tagRuntimes)
{
return InventoryCloneUtility.CloneTagRuntimes(tagRuntimes);
}
private void OnRepoItemDragEnded(object sender, GameEventArgs e)
{
if (!IsEventFromCurrentForm(sender))
@@ -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)
{
@@ -11,6 +11,6 @@ namespace GeometryTD.UI
public int Price;
public Vector3 TargetPos;
public TagType[] Tags;
public string[] TagTexts;
public TagRuntimeData[] TagRuntimes;
}
}
@@ -92,5 +92,84 @@ namespace GeometryTD.CustomUtility
return BuildTagTexts(towerStats.Tags);
}
public static string BuildTagDescriptionText(IReadOnlyList<TagType> tags)
{
if (tags == null || tags.Count <= 0)
{
return string.Empty;
}
List<string> results = new List<string>(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<TagRuntimeData> tagRuntimes)
{
if (tagRuntimes == null || tagRuntimes.Count <= 0)
{
return string.Empty;
}
List<string> results = new List<string>(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;
}
}
}
+204 -3
View File
@@ -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<ShatterTagConfig>());
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<InfernoTagConfig>());
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<AbsoluteZeroTagConfig>());
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<PierceTagConfig>());
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>());
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()
{