S7-03 + S7-04 + S7-05

This commit is contained in:
2026-03-12 11:24:38 +08:00
parent 185ea43323
commit 3840f8e65a
21 changed files with 620 additions and 373 deletions
@@ -1,5 +1,6 @@
using System;
using GeometryTD.CustomUtility;
using UnityEngine;
namespace GeometryTD.Definition
{
@@ -8,6 +9,9 @@ namespace GeometryTD.Definition
{
public int BaseDamage { get; set; }
public AttackPropertyType AttackPropertyType { get; set; }
public int SourceEntityId { get; set; }
public int ProjectileEntityId { get; set; }
public Vector3 OriginPosition { get; set; }
public TagRuntimeData[] TagRuntimes { get; set; } = Array.Empty<TagRuntimeData>();
public AttackPayload Clone()
@@ -16,6 +20,9 @@ namespace GeometryTD.Definition
{
BaseDamage = BaseDamage,
AttackPropertyType = AttackPropertyType,
SourceEntityId = SourceEntityId,
ProjectileEntityId = ProjectileEntityId,
OriginPosition = OriginPosition,
TagRuntimes = InventoryCloneUtility.CloneTagRuntimes(TagRuntimes)
};
}
@@ -1,7 +1,26 @@
using System;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class HitStatusModifierContext
{
public float BonusBurnDurationSeconds { get; set; }
public float BonusBurnDamagePerSecond { get; set; }
public float BonusSlowDurationSeconds { get; set; }
public float BonusSlowRatio { get; set; }
public void Reset()
{
BonusBurnDurationSeconds = 0f;
BonusBurnDamagePerSecond = 0f;
BonusSlowDurationSeconds = 0f;
BonusSlowRatio = 0f;
}
}
[Serializable]
public sealed class HitContext
{
@@ -9,5 +28,35 @@ namespace GeometryTD.Definition
public int FinalDamage { get; set; }
public bool IsCriticalHit { get; set; }
public bool IsKilled { get; set; }
public int TargetEntityId { get; set; }
public Vector3 TargetPosition { get; set; }
public int TargetCurrentHealthBeforeHit { get; set; }
public int TargetCurrentHealthAfterHit { get; set; }
public int TargetMaxHealth { get; set; }
public float TargetMoveSpeedMultiplierBeforeHit { get; set; } = 1f;
public TagType[] TargetStatusTagsBeforeHit { get; set; } = Array.Empty<TagType>();
public EnemyTagStatusRuntime TargetStatusRuntime { get; set; }
public float? CritRoll { get; set; }
public HitStatusModifierContext StatusModifierContext { get; set; } = new HitStatusModifierContext();
public bool HasTargetStatus(TagType tagType)
{
if (tagType == TagType.None || TargetStatusTagsBeforeHit == null || TargetStatusTagsBeforeHit.Length <= 0)
{
return false;
}
for (int i = 0; i < TargetStatusTagsBeforeHit.Length; i++)
{
if (TargetStatusTagsBeforeHit[i] == tagType)
{
return true;
}
}
return false;
}
public bool HasSlowStatusBeforeHit => TargetMoveSpeedMultiplierBeforeHit < 0.999f || HasTargetStatus(TagType.Ice);
}
}
@@ -4,12 +4,7 @@ namespace GeometryTD.Definition
{
public static class NumericTagEffectHandler
{
public static void ApplyBeforeHit(
HitContext hitContext,
int targetCurrentHealth,
int targetMaxHealth,
bool targetHasSlowStatus,
float? critRoll = null)
public static void ApplyBeforeHit(HitContext hitContext)
{
if (hitContext == null || hitContext.AttackPayload == null)
{
@@ -42,21 +37,22 @@ namespace GeometryTD.Definition
switch (runtime.TagType)
{
case TagType.Crit:
ApplyCrit(hitContext, runtime.TotalStack, definition.Config as CritTagConfig, critRoll);
ApplyCrit(hitContext, runtime.TotalStack, definition.Config as CritTagConfig);
break;
case TagType.Execution:
ApplyExecution(hitContext, runtime.TotalStack, targetCurrentHealth, targetMaxHealth, definition.Config as ExecutionTagConfig);
ApplyExecution(hitContext, runtime.TotalStack, definition.Config as ExecutionTagConfig);
break;
case TagType.Shatter:
ApplyShatter(hitContext, runtime.TotalStack, targetHasSlowStatus, definition.Config as ShatterTagConfig);
ApplyShatter(hitContext, runtime.TotalStack, definition.Config as ShatterTagConfig);
break;
}
}
hitContext.IsKilled = targetCurrentHealth > 0 && hitContext.FinalDamage >= targetCurrentHealth;
hitContext.IsKilled = hitContext.TargetCurrentHealthBeforeHit > 0 &&
hitContext.FinalDamage >= hitContext.TargetCurrentHealthBeforeHit;
}
private static void ApplyCrit(HitContext hitContext, int stack, CritTagConfig config, float? critRoll)
private static void ApplyCrit(HitContext hitContext, int stack, CritTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0)
{
@@ -64,7 +60,7 @@ namespace GeometryTD.Definition
}
float chance = Mathf.Clamp01(stack * config.CritChancePerStack);
float resolvedCritRoll = critRoll ?? Random.value;
float resolvedCritRoll = hitContext.CritRoll ?? Random.value;
if (resolvedCritRoll > chance)
{
return;
@@ -77,16 +73,14 @@ namespace GeometryTD.Definition
private static void ApplyExecution(
HitContext hitContext,
int stack,
int targetCurrentHealth,
int targetMaxHealth,
ExecutionTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0 || targetMaxHealth <= 0)
if (config == null || !config.IsImplemented || stack <= 0 || hitContext.TargetMaxHealth <= 0)
{
return;
}
float healthRatio = Mathf.Clamp01((float)Mathf.Max(0, targetCurrentHealth) / targetMaxHealth);
float healthRatio = Mathf.Clamp01((float)Mathf.Max(0, hitContext.TargetCurrentHealthBeforeHit) / hitContext.TargetMaxHealth);
if (healthRatio > config.TargetHealthThreshold)
{
return;
@@ -96,14 +90,14 @@ namespace GeometryTD.Definition
hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier));
}
private static void ApplyShatter(HitContext hitContext, int stack, bool targetHasSlowStatus, ShatterTagConfig config)
private static void ApplyShatter(HitContext hitContext, int stack, ShatterTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0)
{
return;
}
if (config.RequiresSlowedTarget && !targetHasSlowStatus)
if (config.RequiresSlowedTarget && !hitContext.HasSlowStatusBeforeHit)
{
return;
}
@@ -7,11 +7,9 @@ namespace GeometryTD.Definition
{
public abstract TagType TagType { get; }
public virtual void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime,
TagRuntimeData runtimeData)
public virtual void Apply(HitContext hitContext, TagRuntimeData runtimeData)
{
_ = attackPayload;
_ = runtime;
_ = hitContext;
_ = runtimeData;
}
@@ -29,4 +27,4 @@ namespace GeometryTD.Definition
return 1f;
}
}
}
}
@@ -8,10 +8,11 @@ namespace GeometryTD.Definition
{
public override TagType TagType => TagType.Fire;
public override void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime,
TagRuntimeData runtimeData)
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);
@@ -20,11 +21,13 @@ namespace GeometryTD.Definition
return;
}
EnemyTagStatusRuntime runtime = hitContext.TargetStatusRuntime;
FireTagState state = runtime.GetOrCreateState<FireTagState>(TagType);
int infernoStack = TagEffectResolver.GetTagStack(attackPayload?.TagRuntimes, TagType.Inferno);
int effectiveStack = Mathf.Max(0, Mathf.Min(runtimeData.TotalStack, config.MaxEffectiveStack));
HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? new HitStatusModifierContext();
float burnDamagePerSecond = Mathf.Max(
0f,
config.BurnDamagePerSecondPerStack * runtimeData.TotalStack + GetInfernoBonusDamagePerSecond(infernoStack));
config.BurnDamagePerSecondPerStack * effectiveStack + modifierContext.BonusBurnDamagePerSecond);
if (burnDamagePerSecond <= 0f)
{
@@ -33,7 +36,7 @@ namespace GeometryTD.Definition
state.RemainingDuration = Mathf.Max(
state.RemainingDuration,
config.BurnDurationSeconds + GetInfernoBonusDuration(infernoStack));
config.BurnDurationSeconds + modifierContext.BonusBurnDurationSeconds);
state.DamagePerSecond = Mathf.Max(state.DamagePerSecond, burnDamagePerSecond);
runtime.Activate(TagType);
}
@@ -68,39 +71,5 @@ namespace GeometryTD.Definition
state.PendingDamage = 0f;
return false;
}
private static float GetInfernoBonusDuration(int infernoStack)
{
if (infernoStack <= 0 ||
!TagDefinitionRegistry.TryGetDefinition(TagType.Inferno, out TagDefinition 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 ||
!TagDefinitionRegistry.TryGetDefinition(TagType.Inferno, out TagDefinition definition))
{
return 0f;
}
InfernoTagConfig infernoConfig = definition.Config as InfernoTagConfig;
if (infernoConfig == null || !infernoConfig.IsImplemented)
{
return 0f;
}
return infernoStack * infernoConfig.BonusBurnDamagePerSecondPerStack;
}
}
}
@@ -6,8 +6,8 @@ namespace GeometryTD.Definition
public interface IEnemyStatusTagEffect
{
TagType TagType { get; }
void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime, TagRuntimeData runtimeData);
void Apply(HitContext hitContext, TagRuntimeData runtimeData);
bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage);
float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime);
}
}
}
@@ -8,10 +8,10 @@ namespace GeometryTD.Definition
{
public override TagType TagType => TagType.Ice;
public override void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime,
TagRuntimeData runtimeData)
public override void Apply(HitContext hitContext, TagRuntimeData runtimeData)
{
_ = attackPayload;
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));
@@ -23,13 +23,14 @@ namespace GeometryTD.Definition
return;
}
EnemyTagStatusRuntime runtime = hitContext.TargetStatusRuntime;
IceTagState state = runtime.GetOrCreateState<IceTagState>(TagType);
int absoluteZeroStack = TagEffectResolver.GetTagStack(attackPayload?.TagRuntimes, TagType.AbsoluteZero);
float slowRatio = runtimeData.TotalStack * config.SlowRatioPerStack + GetAbsoluteZeroBonusSlowRatio(absoluteZeroStack);
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 + GetAbsoluteZeroBonusDuration(absoluteZeroStack));
config.SlowDurationSeconds + modifierContext.BonusSlowDurationSeconds);
state.SlowMultiplier = Mathf.Min(state.SlowMultiplier, slowMultiplier);
runtime.Activate(TagType);
}
@@ -49,39 +50,5 @@ namespace GeometryTD.Definition
IceTagState state = runtime.GetState<IceTagState>(TagType);
return state == null ? 1f : state.SlowMultiplier;
}
private static float GetAbsoluteZeroBonusDuration(int absoluteZeroStack)
{
if (absoluteZeroStack <= 0 ||
!TagDefinitionRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinition 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 ||
!TagDefinitionRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinition definition))
{
return 0f;
}
AbsoluteZeroTagConfig absoluteZeroConfig = definition.Config as AbsoluteZeroTagConfig;
if (absoluteZeroConfig == null || !absoluteZeroConfig.IsImplemented)
{
return 0f;
}
return absoluteZeroStack * absoluteZeroConfig.BonusSlowRatioPerStack;
}
}
}
@@ -5,37 +5,58 @@ namespace GeometryTD.Definition
{
public static class TagEffectResolver
{
public static HitContext ResolveBeforeHit(
AttackPayload attackPayload,
int targetCurrentHealth,
int targetMaxHealth,
bool targetHasSlowStatus,
float? critRoll = null)
private static readonly HitStatusModifierContext EmptyStatusModifierContext = new HitStatusModifierContext();
public static HitContext ResolveBeforeHit(HitContext hitContext)
{
_ = targetHasSlowStatus;
AttackPayload resolvedPayload = attackPayload?.Clone() ?? new AttackPayload();
HitContext hitContext = new 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 = targetCurrentHealth > 0 && resolvedPayload.BaseDamage >= targetCurrentHealth
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(
hitContext,
targetCurrentHealth,
targetMaxHealth,
targetHasSlowStatus,
critRoll);
return hitContext;
NumericTagEffectHandler.ApplyBeforeHit(resolvedContext);
return resolvedContext;
}
public static void ApplyAfterHit(AttackPayload attackPayload, EnemyTagStatusRuntime targetStatusRuntime)
public static void ApplyAfterHit(HitContext hitContext)
{
if (attackPayload != null && targetStatusRuntime != null)
if (hitContext?.StatusModifierContext == null)
{
TagRuntimeData[] tagRuntimes = attackPayload.TagRuntimes;
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)
@@ -55,15 +76,12 @@ namespace GeometryTD.Definition
continue;
}
effect.Apply(attackPayload, targetStatusRuntime, tag);
effect.Apply(hitContext, tag);
}
}
}
AttackShapeTagEffectHandler.Apply(new HitContext
{
AttackPayload = attackPayload
}, TagTriggerPhase.OnAfterHit);
AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnAfterHit);
}
public static void ApplyOnHit(HitContext hitContext)
@@ -95,5 +113,65 @@ namespace GeometryTD.Definition
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;
}
}
}
@@ -8,8 +8,5 @@ namespace GeometryTD.Definition
public BurnSpreadTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float SpreadRadius { get; set; } = 0f;
public float SpreadDamageRate { get; set; } = 0f;
}
}
}
@@ -8,7 +8,5 @@ namespace GeometryTD.Definition
public FreezeMaskTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float FreezeBuildUpPerStack { get; set; } = 0f;
}
}
}
@@ -8,8 +8,5 @@ namespace GeometryTD.Definition
public IgniteBurstTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float BurstRadius { get; set; } = 0f;
public float BurstDamageRate { get; set; } = 0f;
}
}
}
@@ -8,8 +8,5 @@ namespace GeometryTD.Definition
public OverpenetrateTagConfig(bool isImplemented) : base(isImplemented)
{
}
public int ExtraPenetrationCount { get; set; } = 0;
public float RemainingDamageRate { get; set; } = 0f;
}
}
}
@@ -8,7 +8,5 @@ namespace GeometryTD.Definition
public PierceTagConfig(bool isImplemented) : base(isImplemented)
{
}
public int ExtraPierceCount { get; set; } = 2;
}
}
}
@@ -69,16 +69,16 @@ namespace GeometryTD.Definition
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.OnKill, new BurnSpreadTagConfig(false)),
[TagType.IgniteBurst] = CreateDefinition(TagType.IgniteBurst, TagCategory.AttackShape, TagTriggerPhase.OnKill, new IgniteBurstTagConfig(false)),
[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.OnAfterHit, new FreezeMaskTagConfig(false)),
[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.OnHit, new PierceTagConfig(false)),
[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.OnHit, new OverpenetrateTagConfig(false)),
[TagType.Overpenetrate] = CreateDefinition(TagType.Overpenetrate, TagCategory.AttackShape, TagTriggerPhase.None, new OverpenetrateTagConfig(false)),
[TagType.Execution] = CreateDefinition(TagType.Execution, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ExecutionTagConfig(true))
};
}