This commit is contained in:
2026-04-30 19:33:16 +08:00
commit fb2252f688
556 changed files with 69721 additions and 0 deletions
@@ -0,0 +1,11 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class TagRuntimeData
{
public TagType TagType { get; set; }
public int TotalStack { get; set; }
}
}
@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
namespace GeometryTD.Definition
{
public static class TowerTagAggregationService
{
public static TagRuntimeData[] AggregateTowerTags(params TagType[][] componentTags)
{
Dictionary<TagType, int> stackByTag = new Dictionary<TagType, int>();
if (componentTags != null)
{
foreach (var tags in componentTags)
{
if (tags == null)
{
continue;
}
foreach (var tagType in tags)
{
if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType))
{
continue;
}
if (stackByTag.TryGetValue(tagType, out int stack))
{
stackByTag[tagType] = stack + 1;
}
else
{
stackByTag[tagType] = 1;
}
}
}
}
if (stackByTag.Count <= 0)
{
return Array.Empty<TagRuntimeData>();
}
List<TagRuntimeData> runtimes = new List<TagRuntimeData>(stackByTag.Count);
foreach (var pair in stackByTag)
{
runtimes.Add(new TagRuntimeData
{
TagType = pair.Key,
TotalStack = Math.Max(1, pair.Value)
});
}
runtimes.Sort((left, right) => left.TagType.CompareTo(right.TagType));
return runtimes.ToArray();
}
public static TagRuntimeData[] BuildRuntimeTagsFromUniqueTags(IReadOnlyList<TagType> tags)
{
if (tags == null || tags.Count <= 0)
{
return Array.Empty<TagRuntimeData>();
}
HashSet<TagType> uniqueTags = new HashSet<TagType>();
foreach (var tagType in tags)
{
if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType))
{
continue;
}
uniqueTags.Add(tagType);
}
if (uniqueTags.Count <= 0)
{
return Array.Empty<TagRuntimeData>();
}
TagType[] orderedTags = new TagType[uniqueTags.Count];
uniqueTags.CopyTo(orderedTags);
Array.Sort(orderedTags);
TagRuntimeData[] runtimes = new TagRuntimeData[orderedTags.Length];
for (int i = 0; i < orderedTags.Length; i++)
{
runtimes[i] = new TagRuntimeData
{
TagType = orderedTags[i],
TotalStack = 1
};
}
return runtimes;
}
public static TagType[] FlattenUniqueTags(IReadOnlyList<TagRuntimeData> tagRuntimes)
{
if (tagRuntimes == null || tagRuntimes.Count <= 0)
{
return Array.Empty<TagType>();
}
List<TagType> tags = new List<TagType>(tagRuntimes.Count);
foreach (var runtime in tagRuntimes)
{
if (runtime == null || runtime.TagType == TagType.None || !Enum.IsDefined(typeof(TagType), runtime.TagType))
{
continue;
}
if (runtime.TotalStack <= 0)
{
continue;
}
tags.Add(runtime.TagType);
}
if (tags.Count <= 0)
{
return Array.Empty<TagType>();
}
tags.Sort();
return tags.ToArray();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class EnemyStatusTagRegistry
{
// Only tags that create or tick enemy-held status state belong here.
private static readonly Dictionary<TagType, IEnemyStatusTagEffect> EffectsByTag =
new Dictionary<TagType, IEnemyStatusTagEffect>
{
[TagType.Fire] = new FireTagEffect(),
[TagType.Ice] = new IceTagEffect()
};
public static IReadOnlyDictionary<TagType, IEnemyStatusTagEffect> Effects => EffectsByTag;
public static bool TryGetEffect(TagType tagType, out IEnemyStatusTagEffect effect)
{
return EffectsByTag.TryGetValue(tagType, out effect);
}
}
}
@@ -0,0 +1,41 @@
namespace GeometryTD.Definition
{
public static class AttackShapeTagEffectHandler
{
public static void Apply(HitContext hitContext, TagTriggerPhase triggerPhase)
{
if (hitContext?.AttackPayload?.TagRuntimes == null || hitContext.AttackPayload.TagRuntimes.Length <= 0)
{
return;
}
for (int i = 0; i < hitContext.AttackPayload.TagRuntimes.Length; i++)
{
TagRuntimeData runtime = hitContext.AttackPayload.TagRuntimes[i];
if (runtime == null || runtime.TotalStack <= 0)
{
continue;
}
if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) ||
definition == null ||
definition.Category != TagCategory.AttackShape ||
definition.TriggerPhase != triggerPhase ||
definition.Config == null)
{
continue;
}
switch (runtime.TagType)
{
case TagType.BurnSpread:
case TagType.IgniteBurst:
case TagType.FreezeMask:
case TagType.Pierce:
case TagType.Overpenetrate:
break;
}
}
}
}
}
@@ -0,0 +1,109 @@
using UnityEngine;
namespace GeometryTD.Definition
{
public static class NumericTagEffectHandler
{
public static void ApplyBeforeHit(HitContext hitContext)
{
if (hitContext == null || hitContext.AttackPayload == null)
{
return;
}
TagRuntimeData[] tagRuntimes = hitContext.AttackPayload.TagRuntimes;
if (tagRuntimes == null || tagRuntimes.Length <= 0)
{
return;
}
for (int i = 0; i < tagRuntimes.Length; i++)
{
TagRuntimeData runtime = tagRuntimes[i];
if (runtime == null || runtime.TotalStack <= 0)
{
continue;
}
if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) ||
definition == null ||
definition.Category != TagCategory.NumericModifier ||
definition.TriggerPhase != TagTriggerPhase.OnBeforeHit ||
definition.Config == null)
{
continue;
}
switch (runtime.TagType)
{
case TagType.Crit:
ApplyCrit(hitContext, runtime.TotalStack, definition.Config as CritTagConfig);
break;
case TagType.Execution:
ApplyExecution(hitContext, runtime.TotalStack, definition.Config as ExecutionTagConfig);
break;
case TagType.Shatter:
ApplyShatter(hitContext, runtime.TotalStack, definition.Config as ShatterTagConfig);
break;
}
}
hitContext.IsKilled = hitContext.TargetCurrentHealthBeforeHit > 0 &&
hitContext.FinalDamage >= hitContext.TargetCurrentHealthBeforeHit;
}
private static void ApplyCrit(HitContext hitContext, int stack, CritTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0)
{
return;
}
float chance = Mathf.Clamp01(stack * config.CritChancePerStack);
float resolvedCritRoll = hitContext.CritRoll ?? Random.value;
if (resolvedCritRoll > chance)
{
return;
}
hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * config.CritDamageMultiplier));
hitContext.IsCriticalHit = true;
}
private static void ApplyExecution(
HitContext hitContext,
int stack,
ExecutionTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0 || hitContext.TargetMaxHealth <= 0)
{
return;
}
float healthRatio = Mathf.Clamp01((float)Mathf.Max(0, hitContext.TargetCurrentHealthBeforeHit) / hitContext.TargetMaxHealth);
if (healthRatio > config.TargetHealthThreshold)
{
return;
}
float multiplier = 1f + stack * config.DamageBonusPerStack;
hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier));
}
private static void ApplyShatter(HitContext hitContext, int stack, ShatterTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0)
{
return;
}
if (config.RequiresSlowedTarget && !hitContext.HasSlowStatusBeforeHit)
{
return;
}
float multiplier = 1f + stack * config.DamageBonusPerStack;
hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier));
}
}
}
@@ -0,0 +1,7 @@
namespace GeometryTD.Definition
{
public abstract class EnemyStatusTagStateBase
{
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public sealed class FireTagState : EnemyStatusTagStateBase
{
public float RemainingDuration { get; set; }
public float DamagePerSecond { get; set; }
public float PendingDamage { get; set; }
}
}
@@ -0,0 +1,8 @@
namespace GeometryTD.Definition
{
public sealed class IceTagState : EnemyStatusTagStateBase
{
public float RemainingDuration { get; set; }
public float SlowMultiplier { get; set; } = 1f;
}
}
@@ -0,0 +1,30 @@
using System;
using GeometryTD.Entity;
namespace GeometryTD.Definition
{
public abstract class EnemyStatusTagEffectBase : IEnemyStatusTagEffect
{
public abstract TagType TagType { get; }
public virtual void Apply(HitContext hitContext, TagRuntimeData runtimeData)
{
_ = hitContext;
_ = runtimeData;
}
public virtual bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage)
{
_ = runtime;
_ = deltaTime;
_ = applyDamage;
return false;
}
public virtual float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime)
{
_ = runtime;
return 1f;
}
}
}
@@ -0,0 +1,75 @@
using System;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
public sealed class FireTagEffect : EnemyStatusTagEffectBase
{
public override TagType TagType => TagType.Fire;
public override void Apply(HitContext hitContext, TagRuntimeData runtimeData)
{
Debug.Assert(TagDefinitionRegistry.TryGetDefinition(TagType, out TagDefinition definition));
Debug.Assert(hitContext != null);
Debug.Assert(hitContext.TargetStatusRuntime != null);
FireTagConfig config = definition.Config as FireTagConfig;
Debug.Assert(config != null);
if (!config.IsImplemented)
{
return;
}
EnemyTagStatusRuntime runtime = hitContext.TargetStatusRuntime;
FireTagState state = runtime.GetOrCreateState<FireTagState>(TagType);
int effectiveStack = Mathf.Max(0, Mathf.Min(runtimeData.TotalStack, config.MaxEffectiveStack));
HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? new HitStatusModifierContext();
float burnDamagePerSecond = Mathf.Max(
0f,
config.BurnDamagePerSecondPerStack * effectiveStack + modifierContext.BonusBurnDamagePerSecond);
if (burnDamagePerSecond <= 0f)
{
return;
}
state.RemainingDuration = Mathf.Max(
state.RemainingDuration,
config.BurnDurationSeconds + modifierContext.BonusBurnDurationSeconds);
state.DamagePerSecond = Mathf.Max(state.DamagePerSecond, burnDamagePerSecond);
runtime.Activate(TagType);
}
public override bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage)
{
FireTagState state = runtime.GetState<FireTagState>(TagType);
Debug.Assert(state != null);
float resolvedDeltaTime = Mathf.Max(0f, deltaTime);
if (resolvedDeltaTime <= 0f || state.RemainingDuration <= 0f)
{
return state.RemainingDuration > 0f;
}
float appliedDuration = Mathf.Min(resolvedDeltaTime, state.RemainingDuration);
state.RemainingDuration = Mathf.Max(0f, state.RemainingDuration - appliedDuration);
state.PendingDamage += state.DamagePerSecond * appliedDuration;
int resolvedDamage = Mathf.FloorToInt(state.PendingDamage);
if (resolvedDamage > 0)
{
applyDamage?.Invoke(resolvedDamage);
state.PendingDamage -= resolvedDamage;
}
if (state.RemainingDuration > 0f)
{
return true;
}
state.PendingDamage = 0f;
return false;
}
}
}
@@ -0,0 +1,13 @@
using System;
using GeometryTD.Entity;
namespace GeometryTD.Definition
{
public interface IEnemyStatusTagEffect
{
TagType TagType { get; }
void Apply(HitContext hitContext, TagRuntimeData runtimeData);
bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage);
float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime);
}
}
@@ -0,0 +1,54 @@
using System;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
public sealed class IceTagEffect : EnemyStatusTagEffectBase
{
public override TagType TagType => TagType.Ice;
public override void Apply(HitContext hitContext, TagRuntimeData runtimeData)
{
Debug.Assert(hitContext != null);
Debug.Assert(hitContext.TargetStatusRuntime != null);
Debug.Assert(runtimeData != null);
Debug.Assert(runtimeData.TotalStack > 0);
Debug.Assert(TagDefinitionRegistry.TryGetDefinition(TagType, out TagDefinition definition));
IceTagConfig config = definition.Config as IceTagConfig;
Debug.Assert(config != null);
if (!config.IsImplemented)
{
return;
}
EnemyTagStatusRuntime runtime = hitContext.TargetStatusRuntime;
IceTagState state = runtime.GetOrCreateState<IceTagState>(TagType);
HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? new HitStatusModifierContext();
float slowRatio = runtimeData.TotalStack * config.SlowRatioPerStack + modifierContext.BonusSlowRatio;
float slowMultiplier = Mathf.Max(config.MinMoveSpeedMultiplier, 1f - slowRatio);
state.RemainingDuration = Mathf.Max(
state.RemainingDuration,
config.SlowDurationSeconds + modifierContext.BonusSlowDurationSeconds);
state.SlowMultiplier = Mathf.Min(state.SlowMultiplier, slowMultiplier);
runtime.Activate(TagType);
}
public override bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage)
{
_ = applyDamage;
IceTagState state = runtime.GetState<IceTagState>(TagType);
Debug.Assert(state != null);
state.RemainingDuration = Mathf.Max(0f, state.RemainingDuration - Mathf.Max(0f, deltaTime));
return state.RemainingDuration > 0f;
}
public override float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime)
{
IceTagState state = runtime.GetState<IceTagState>(TagType);
return state == null ? 1f : state.SlowMultiplier;
}
}
}
@@ -0,0 +1,177 @@
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class TagEffectResolver
{
private static readonly HitStatusModifierContext EmptyStatusModifierContext = new HitStatusModifierContext();
public static HitContext ResolveBeforeHit(HitContext hitContext)
{
if (hitContext == null)
{
return new HitContext
{
AttackPayload = new AttackPayload()
};
}
AttackPayload resolvedPayload = hitContext.AttackPayload?.Clone() ?? new AttackPayload();
HitContext resolvedContext = new HitContext
{
AttackPayload = resolvedPayload,
FinalDamage = Mathf.Max(0, resolvedPayload.BaseDamage),
IsCriticalHit = false,
IsKilled = hitContext.TargetCurrentHealthBeforeHit > 0 &&
resolvedPayload.BaseDamage >= hitContext.TargetCurrentHealthBeforeHit,
TargetEntityId = hitContext.TargetEntityId,
TargetPosition = hitContext.TargetPosition,
TargetCurrentHealthBeforeHit = hitContext.TargetCurrentHealthBeforeHit,
TargetCurrentHealthAfterHit = hitContext.TargetCurrentHealthAfterHit,
TargetMaxHealth = hitContext.TargetMaxHealth,
TargetMoveSpeedMultiplierBeforeHit = hitContext.TargetMoveSpeedMultiplierBeforeHit,
TargetStatusTagsBeforeHit = hitContext.TargetStatusTagsBeforeHit ?? System.Array.Empty<TagType>(),
TargetStatusRuntime = hitContext.TargetStatusRuntime,
CritRoll = hitContext.CritRoll,
StatusModifierContext = new HitStatusModifierContext()
};
NumericTagEffectHandler.ApplyBeforeHit(resolvedContext);
return resolvedContext;
}
public static void ApplyAfterHit(HitContext hitContext)
{
if (hitContext?.StatusModifierContext == null)
{
if (hitContext != null)
{
hitContext.StatusModifierContext = new HitStatusModifierContext();
}
}
hitContext?.StatusModifierContext?.Reset();
ApplyStatusModifiers(hitContext);
if (hitContext?.AttackPayload != null && hitContext.TargetStatusRuntime != null)
{
TagRuntimeData[] tagRuntimes = hitContext.AttackPayload.TagRuntimes;
if (tagRuntimes != null && tagRuntimes.Length > 0)
{
foreach (var tag in tagRuntimes)
{
if (tag == null || tag.TotalStack <= 0)
{
continue;
}
if (!TagDefinitionRegistry.TryGetDefinition(tag.TagType, out TagDefinition definition) ||
definition == null ||
definition.Category != TagCategory.Status ||
definition.TriggerPhase != TagTriggerPhase.OnAfterHit ||
definition.Config == null ||
!EnemyStatusTagRegistry.TryGetEffect(tag.TagType, out IEnemyStatusTagEffect effect))
{
continue;
}
effect.Apply(hitContext, tag);
}
}
}
AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnAfterHit);
}
public static void ApplyOnHit(HitContext hitContext)
{
AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnHit);
}
public static void ApplyOnKill(HitContext hitContext)
{
AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnKill);
}
public static int GetTagStack(TagRuntimeData[] tagRuntimes, TagType tagType)
{
if (tagRuntimes == null || tagRuntimes.Length <= 0 || tagType == TagType.None)
{
return 0;
}
foreach (var tag in tagRuntimes)
{
if (tag == null || tag.TagType != tagType || tag.TotalStack <= 0)
{
continue;
}
return tag.TotalStack;
}
return 0;
}
private static void ApplyStatusModifiers(HitContext hitContext)
{
if (hitContext?.AttackPayload?.TagRuntimes == null || hitContext.AttackPayload.TagRuntimes.Length <= 0)
{
return;
}
HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? EmptyStatusModifierContext;
for (int i = 0; i < hitContext.AttackPayload.TagRuntimes.Length; i++)
{
TagRuntimeData runtime = hitContext.AttackPayload.TagRuntimes[i];
if (runtime == null || runtime.TotalStack <= 0)
{
continue;
}
if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) ||
definition == null ||
definition.Category != TagCategory.StatusModifier ||
definition.TriggerPhase != TagTriggerPhase.OnAfterHit ||
definition.Config == null ||
!definition.Config.IsImplemented)
{
continue;
}
switch (runtime.TagType)
{
case TagType.Inferno:
ApplyInfernoModifier(runtime.TotalStack, definition.Config as InfernoTagConfig, modifierContext);
break;
case TagType.AbsoluteZero:
ApplyAbsoluteZeroModifier(runtime.TotalStack, definition.Config as AbsoluteZeroTagConfig, modifierContext);
break;
}
}
}
private static void ApplyInfernoModifier(int stack, InfernoTagConfig config, HitStatusModifierContext modifierContext)
{
if (config == null || stack <= 0)
{
return;
}
modifierContext.BonusBurnDurationSeconds += stack * config.BonusBurnDurationSeconds;
modifierContext.BonusBurnDamagePerSecond += stack * config.BonusBurnDamagePerSecondPerStack;
}
private static void ApplyAbsoluteZeroModifier(int stack, AbsoluteZeroTagConfig config, HitStatusModifierContext modifierContext)
{
if (config == null || stack <= 0)
{
return;
}
modifierContext.BonusSlowDurationSeconds += stack * config.BonusSlowDurationSeconds;
modifierContext.BonusSlowRatio += stack * config.BonusSlowRatioPerStack;
}
}
}
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Random = System.Random;
namespace GeometryTD.Definition
{
public static class ComponentTagGenerationService
{
public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
InventoryTagRandomContext randomContext,
IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{
IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules;
TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, ruleLookup);
if (eligibleTags.Length <= 0)
{
return Array.Empty<TagType>();
}
Random random = new Random(BuildStableSeed(rarity, randomContext));
int tagBudget = ResolveRarityTagBudget(rarity, random, rarityTagBudgetRulesByRarity);
if (tagBudget <= 0)
{
return Array.Empty<TagType>();
}
int finalCount = Math.Min(tagBudget, eligibleTags.Length);
List<TagType> pool = new List<TagType>(eligibleTags);
TagType[] result = new TagType[finalCount];
for (int i = 0; i < finalCount; i++)
{
int index = RollWeightedIndex(pool, ruleLookup, random);
result[i] = pool[index];
pool.RemoveAt(index);
}
return result;
}
public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
InventoryTagSourceType sourceType,
long itemInstanceId,
int configId,
IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{
return ResolveComponentTags(
possibleTags,
rarity,
new InventoryTagRandomContext(0, sourceType, itemInstanceId, configId),
rulesByTag,
rarityTagBudgetRulesByRarity);
}
public static TagType[] GetEligibleTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null)
{
if (possibleTags == null || possibleTags.Count <= 0)
{
return Array.Empty<TagType>();
}
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules;
HashSet<TagType> uniqueTags = new HashSet<TagType>();
for (int i = 0; i < possibleTags.Count; i++)
{
TagType tagType = possibleTags[i];
if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType) || !IsSupportedLaunchTag(tagType))
{
continue;
}
if (!TryGetRule(tagType, ruleLookup, out TagGenerationRule rule))
{
continue;
}
if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(rule.MinRarity))
{
continue;
}
uniqueTags.Add(tagType);
}
if (uniqueTags.Count <= 0)
{
return Array.Empty<TagType>();
}
TagType[] result = new TagType[uniqueTags.Count];
uniqueTags.CopyTo(result);
Array.Sort(result);
return result;
}
public static int ResolveRarityTagBudget(
RarityType rarity,
Random random,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> ruleLookup = rarityTagBudgetRulesByRarity ?? RarityTagBudgetRuleRegistry.Rules;
RarityTagBudgetRule rule = GetRarityTagBudgetRule(normalizedRarity, ruleLookup);
Debug.Assert(rule.MinCount >= 0);
Debug.Assert(rule.MaxCount >= rule.MinCount);
if (rule.MinCount == rule.MaxCount)
{
return rule.MinCount;
}
random ??= new Random(0);
return random.Next(rule.MinCount, rule.MaxCount + 1);
}
private static bool IsSupportedLaunchTag(TagType tagType)
{
return TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition) &&
definition.Config != null &&
definition.Config.IsImplemented;
}
private static bool TryGetRule(
TagType tagType,
IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup,
out TagGenerationRule rule)
{
if (ruleLookup != null && ruleLookup.TryGetValue(tagType, out rule))
{
return true;
}
rule = null;
return false;
}
private static RarityTagBudgetRule GetRarityTagBudgetRule(
RarityType rarity,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> ruleLookup)
{
Debug.Assert(ruleLookup != null);
Debug.Assert(ruleLookup.TryGetValue(rarity, out _));
return ruleLookup[rarity];
}
private static int RollWeightedIndex(
IReadOnlyList<TagType> pool,
IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup,
Random random)
{
int totalWeight = 0;
for (int i = 0; i < pool.Count; i++)
{
TagGenerationRule rule = ruleLookup[pool[i]];
Debug.Assert(rule.Weight > 0);
totalWeight += rule.Weight;
}
Debug.Assert(totalWeight > 0);
int roll = random.Next(1, totalWeight + 1);
int cumulative = 0;
for (int i = 0; i < pool.Count; i++)
{
TagGenerationRule rule = ruleLookup[pool[i]];
cumulative += rule.Weight;
if (roll <= cumulative)
{
return i;
}
}
return pool.Count - 1;
}
private static int BuildStableSeed(RarityType rarity, InventoryTagRandomContext randomContext)
{
unchecked
{
int seed = 17;
seed = seed * 31 + randomContext.RunSeed;
seed = seed * 31 + (int)InventoryRarityRuleService.NormalizeComponentRarity(rarity);
seed = seed * 31 + (int)randomContext.SourceType;
seed = seed * 31 + randomContext.ConfigId;
seed = seed * 31 + (int)randomContext.ItemInstanceId;
seed = seed * 31 + (int)(randomContext.ItemInstanceId >> 32);
return seed;
}
}
}
}
@@ -0,0 +1,72 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public readonly struct InventoryTagRandomContext
{
public InventoryTagRandomContext(int runSeed, InventoryTagSourceType sourceType, long itemInstanceId, int configId)
{
RunSeed = runSeed;
SourceType = sourceType;
ItemInstanceId = itemInstanceId;
ConfigId = configId;
}
public int RunSeed { get; }
public InventoryTagSourceType SourceType { get; }
public long ItemInstanceId { get; }
public int ConfigId { get; }
public static InventoryTagRandomContext CreateSeed(int runSeed, long itemInstanceId, int configId)
{
return new InventoryTagRandomContext(runSeed, InventoryTagSourceType.Seed, itemInstanceId, configId);
}
public static InventoryTagRandomContext CreateShop(int runSeed, int nodeSequenceIndex, int goodsIndex, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Shop,
ComposeLocalItemInstanceId(nodeSequenceIndex, goodsIndex),
configId);
}
public static InventoryTagRandomContext CreateDrop(int runSeed, int nodeSequenceIndex, int dropOrdinal, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Drop,
ComposeLocalItemInstanceId(nodeSequenceIndex, dropOrdinal),
configId);
}
public static InventoryTagRandomContext CreateReward(int runSeed, int nodeSequenceIndex, int rewardOrdinal, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Reward,
ComposeLocalItemInstanceId(nodeSequenceIndex, rewardOrdinal),
configId);
}
public static InventoryTagRandomContext CreateEvent(int runSeed, int nodeSequenceIndex, int eventOrdinal, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Event,
ComposeLocalItemInstanceId(nodeSequenceIndex, eventOrdinal),
configId);
}
public static long ComposeLocalItemInstanceId(int nodeSequenceIndex, int localOrdinal)
{
long normalizedSequence = Math.Max(0, nodeSequenceIndex) + 1L;
long normalizedOrdinal = Math.Max(0, localOrdinal) + 1L;
return (normalizedSequence << 32) | (uint)normalizedOrdinal;
}
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public sealed class RarityTagBudgetRule
{
public RarityType Rarity { get; set; }
public int MinCount { get; set; }
public int MaxCount { get; set; }
}
}
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class RarityTagBudgetRuleRegistry
{
private static readonly Dictionary<RarityType, RarityTagBudgetRule> RulesByRarity = CreateDefaultRules();
public static IReadOnlyDictionary<RarityType, RarityTagBudgetRule> Rules => RulesByRarity;
public static void ResetToDefaults()
{
RulesByRarity.Clear();
foreach (KeyValuePair<RarityType, RarityTagBudgetRule> pair in CreateDefaultRules())
{
RulesByRarity.Add(pair.Key, pair.Value);
}
}
public static void LoadFromRows(IEnumerable<DRRarityTagBudget> rows)
{
ResetToDefaults();
foreach (DRRarityTagBudget row in rows)
{
ApplyRow(row);
}
}
public static bool TryGetRule(RarityType rarity, out RarityTagBudgetRule rule)
{
return RulesByRarity.TryGetValue(rarity, out rule);
}
private static Dictionary<RarityType, RarityTagBudgetRule> CreateDefaultRules()
{
return new Dictionary<RarityType, RarityTagBudgetRule>
{
[RarityType.White] = CreateRule(RarityType.White, 0, 1),
[RarityType.Green] = CreateRule(RarityType.Green, 0, 2),
[RarityType.Blue] = CreateRule(RarityType.Blue, 1, 3),
[RarityType.Purple] = CreateRule(RarityType.Purple, 1, 3),
[RarityType.Red] = CreateRule(RarityType.Red, 2, 4)
};
}
private static RarityTagBudgetRule CreateRule(RarityType rarity, int minCount, int maxCount)
{
Debug.Assert(rarity >= RarityType.White && rarity <= RarityType.Red);
Debug.Assert(minCount >= 0);
Debug.Assert(maxCount >= minCount);
return new RarityTagBudgetRule
{
Rarity = rarity,
MinCount = minCount,
MaxCount = maxCount
};
}
private static void ApplyRow(DRRarityTagBudget row)
{
Debug.Assert(row != null);
Debug.Assert(row.Id > 0);
Debug.Assert(row.Rarity >= RarityType.White && row.Rarity <= RarityType.Red);
Debug.Assert(row.MinCount >= 0);
Debug.Assert(row.MaxCount >= row.MinCount);
RulesByRarity[row.Rarity] = CreateRule(row.Rarity, row.MinCount, row.MaxCount);
}
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public sealed class TagGenerationRule
{
public TagType TagType { get; set; }
public RarityType MinRarity { get; set; }
public int Weight { get; set; }
}
}
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class TagGenerationRuleRegistry
{
private static readonly Dictionary<TagType, TagGenerationRule> RulesByTag = CreateDefaultRules();
public static IReadOnlyDictionary<TagType, TagGenerationRule> Rules => RulesByTag;
public static void ResetToDefaults()
{
RulesByTag.Clear();
foreach (KeyValuePair<TagType, TagGenerationRule> pair in CreateDefaultRules())
{
RulesByTag.Add(pair.Key, pair.Value);
}
}
public static void LoadFromRows(IEnumerable<DRTag> rows)
{
ResetToDefaults();
foreach (DRTag row in rows)
{
ApplyRow(row);
}
}
public static bool TryGetRule(TagType tagType, out TagGenerationRule rule)
{
return RulesByTag.TryGetValue(tagType, out rule);
}
private static Dictionary<TagType, TagGenerationRule> CreateDefaultRules()
{
return new Dictionary<TagType, TagGenerationRule>
{
[TagType.Fire] = CreateRule(TagType.Fire, RarityType.White, 20),
[TagType.BurnSpread] = CreateRule(TagType.BurnSpread, RarityType.White, 20),
[TagType.IgniteBurst] = CreateRule(TagType.IgniteBurst, RarityType.Green, 15),
[TagType.Inferno] = CreateRule(TagType.Inferno, RarityType.Purple, 5),
[TagType.Ice] = CreateRule(TagType.Ice, RarityType.White, 1),
[TagType.FreezeMask] = CreateRule(TagType.FreezeMask, RarityType.White, 20),
[TagType.Shatter] = CreateRule(TagType.Shatter, RarityType.Green, 15),
[TagType.AbsoluteZero] = CreateRule(TagType.AbsoluteZero, RarityType.Purple, 1),
[TagType.Pierce] = CreateRule(TagType.Pierce, RarityType.White, 20),
[TagType.Crit] = CreateRule(TagType.Crit, RarityType.White, 20),
[TagType.Overpenetrate] = CreateRule(TagType.Overpenetrate, RarityType.Green, 15),
[TagType.Execution] = CreateRule(TagType.Execution, RarityType.Purple, 5)
};
}
private static TagGenerationRule CreateRule(TagType tagType, RarityType minRarity, int weight)
{
return new TagGenerationRule
{
TagType = tagType,
MinRarity = minRarity,
Weight = weight
};
}
private static void ApplyRow(DRTag row)
{
Debug.Assert(row != null);
Debug.Assert(row.Id > 0);
Debug.Assert(row.Weight > 0);
RulesByTag[row.TagType] = CreateRule(row.TagType, row.MinRarity, row.Weight);
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class AbsoluteZeroTagConfig : TagConfigBase
{
public AbsoluteZeroTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float BonusSlowDurationSeconds { get; set; } = 1f;
public float BonusSlowRatioPerStack { get; set; } = 0.1f;
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class BurnSpreadTagConfig : TagConfigBase
{
public BurnSpreadTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class CritTagConfig : TagConfigBase
{
public CritTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float CritChancePerStack { get; set; } = 0.1f;
public float CritDamageMultiplier { get; set; } = 1.5f;
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class ExecutionTagConfig : TagConfigBase
{
public ExecutionTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float TargetHealthThreshold { get; set; } = 0.3f;
public float DamageBonusPerStack { get; set; } = 0.5f;
}
}
@@ -0,0 +1,16 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class FireTagConfig : TagConfigBase
{
public FireTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float BurnDurationSeconds { get; set; } = 3f;
public float BurnDamagePerSecondPerStack { get; set; } = 20f;
public int MaxEffectiveStack { get; set; } = 5;
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class FreezeMaskTagConfig : TagConfigBase
{
public FreezeMaskTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,16 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class IceTagConfig : TagConfigBase
{
public IceTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float SlowDurationSeconds { get; set; } = 2f;
public float SlowRatioPerStack { get; set; } = 0.2f;
public float MinMoveSpeedMultiplier { get; set; } = 0.4f;
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class IgniteBurstTagConfig : TagConfigBase
{
public IgniteBurstTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class InfernoTagConfig : TagConfigBase
{
public InfernoTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float BonusBurnDurationSeconds { get; set; } = 2f;
public float BonusBurnDamagePerSecondPerStack { get; set; } = 20f;
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class OverpenetrateTagConfig : TagConfigBase
{
public OverpenetrateTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class PierceTagConfig : TagConfigBase
{
public PierceTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class ShatterTagConfig : TagConfigBase
{
public ShatterTagConfig(bool isImplemented) : base(isImplemented)
{
}
public bool RequiresSlowedTarget { get; set; } = true;
public float DamageBonusPerStack { get; set; } = 0.25f;
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public abstract class TagConfigBase
{
public bool IsImplemented { get; set; }
protected TagConfigBase(bool isImplemented)
{
IsImplemented = isImplemented;
}
}
}
@@ -0,0 +1,14 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class TagDefinition
{
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; }
}
}
@@ -0,0 +1,214 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class TagDefinitionRegistry
{
private static readonly Dictionary<TagType, TagDefinition> DefinitionsByTag = CreateDefaultDefinitions();
public static IReadOnlyDictionary<TagType, TagDefinition> Definitions => DefinitionsByTag;
public static void ResetToDefaults()
{
DefinitionsByTag.Clear();
foreach (KeyValuePair<TagType, TagDefinition> pair in CreateDefaultDefinitions())
{
DefinitionsByTag.Add(pair.Key, pair.Value);
}
}
public static void LoadFromRows(IEnumerable<DRTagConfig> rows)
{
ReloadFromRows(rows, null);
}
public static bool TryGetDefinition(TagType tagType, out TagDefinition definition)
{
return DefinitionsByTag.TryGetValue(tagType, out definition);
}
public static void ApplyTagRows(IEnumerable<DRTag> rows)
{
if (rows == null)
{
return;
}
foreach (DRTag row in rows)
{
ApplyTagRow(row);
}
}
public static void ReloadFromRows(IEnumerable<DRTagConfig> tagConfigRows, IEnumerable<DRTag> tagRows)
{
ResetToDefaults();
if (tagConfigRows != null)
{
foreach (DRTagConfig row in tagConfigRows)
{
ApplyRow(row);
}
}
if (tagRows != null)
{
foreach (DRTag row in tagRows)
{
ApplyTagRow(row);
}
}
}
private static Dictionary<TagType, TagDefinition> CreateDefaultDefinitions()
{
return new Dictionary<TagType, TagDefinition>
{
[TagType.Fire] = CreateDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit, new FireTagConfig(true)),
[TagType.BurnSpread] = CreateDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.None, new BurnSpreadTagConfig(false)),
[TagType.IgniteBurst] = CreateDefinition(TagType.IgniteBurst, TagCategory.AttackShape, TagTriggerPhase.None, new IgniteBurstTagConfig(false)),
[TagType.Inferno] = CreateDefinition(TagType.Inferno, TagCategory.StatusModifier, 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.None, new FreezeMaskTagConfig(false)),
[TagType.Shatter] = CreateDefinition(TagType.Shatter, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ShatterTagConfig(true)),
[TagType.AbsoluteZero] = CreateDefinition(TagType.AbsoluteZero, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit, new AbsoluteZeroTagConfig(true)),
[TagType.Pierce] = CreateDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.None, new PierceTagConfig(false)),
[TagType.Crit] = CreateDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new CritTagConfig(true)),
[TagType.Overpenetrate] = CreateDefinition(TagType.Overpenetrate, TagCategory.AttackShape, TagTriggerPhase.None, new OverpenetrateTagConfig(false)),
[TagType.Execution] = CreateDefinition(TagType.Execution, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ExecutionTagConfig(true))
};
}
private static TagDefinition CreateDefinition(
TagType tagType,
TagCategory category,
TagTriggerPhase triggerPhase,
TagConfigBase config)
{
return new TagDefinition
{
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 TagDefinition 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 ApplyTagRow(DRTag row)
{
Debug.Assert(row != null);
Debug.Assert(row.Id > 0);
Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinition definition));
Debug.Assert(definition.Config != null);
definition.Config.IsImplemented = row.IsImplemented;
}
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>();
}
}
}
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
namespace GeometryTD.CustomUtility
{
public static class TagDisplayUtility
{
public static string ResolveTagName(TagType tagType)
{
if (tagType == TagType.None)
{
return string.Empty;
}
if (GameEntry.DataTable != null)
{
var tagTable = GameEntry.DataTable.GetDataTable<DRTag>();
if (tagTable != null)
{
DRTag tagRow = tagTable.GetDataRow((int)tagType);
if (tagRow != null && !string.IsNullOrWhiteSpace(tagRow.Name))
{
return tagRow.Name;
}
}
}
return tagType.ToString();
}
public static string[] BuildTagTexts(IReadOnlyList<TagType> tags)
{
if (tags == null || tags.Count <= 0)
{
return Array.Empty<string>();
}
List<string> results = new List<string>(tags.Count);
for (int i = 0; i < tags.Count; i++)
{
string tagName = ResolveTagName(tags[i]);
if (!string.IsNullOrWhiteSpace(tagName))
{
results.Add(tagName);
}
}
return results.ToArray();
}
public static string[] BuildTagTexts(IReadOnlyList<TagRuntimeData> tagRuntimes)
{
if (tagRuntimes == null || tagRuntimes.Count <= 0)
{
return Array.Empty<string>();
}
List<string> results = new List<string>(tagRuntimes.Count);
for (int i = 0; i < tagRuntimes.Count; i++)
{
TagRuntimeData tagRuntime = tagRuntimes[i];
if (tagRuntime == null || tagRuntime.TagType == TagType.None || tagRuntime.TotalStack <= 0)
{
continue;
}
string tagName = ResolveTagName(tagRuntime.TagType);
if (string.IsNullOrWhiteSpace(tagName))
{
continue;
}
results.Add(tagRuntime.TotalStack > 1 ? $"{tagName} x{tagRuntime.TotalStack}" : tagName);
}
return results.ToArray();
}
public static string[] BuildTowerTagTexts(TowerStatsData towerStats)
{
if (towerStats == null)
{
return Array.Empty<string>();
}
if (towerStats.TagRuntimes != null && towerStats.TagRuntimes.Length > 0)
{
return BuildTagTexts(towerStats.TagRuntimes);
}
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 (TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition) &&
!string.IsNullOrWhiteSpace(definition.Description))
{
return definition.Description;
}
return string.Empty;
}
}
}