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
+229 -18
View File
@@ -24,7 +24,7 @@ namespace GeometryTD.Tests.EditMode
}
};
HitContext hitContext = TagEffectResolver.ResolveBeforeHit(attackPayload, 100, 100, false, 0.05f);
HitContext hitContext = TagEffectResolver.ResolveBeforeHit(CreateHitContext(attackPayload, 100, 100, 1f, null, 0.05f));
Assert.That(hitContext.IsCriticalHit, Is.True);
Assert.That(hitContext.FinalDamage, Is.EqualTo(150));
@@ -43,8 +43,8 @@ namespace GeometryTD.Tests.EditMode
}
};
HitContext lowHealthHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 30, 100, false);
HitContext highHealthHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 31, 100, false);
HitContext lowHealthHit = TagEffectResolver.ResolveBeforeHit(CreateHitContext(attackPayload, 30, 100));
HitContext highHealthHit = TagEffectResolver.ResolveBeforeHit(CreateHitContext(attackPayload, 31, 100));
Assert.That(lowHealthHit.FinalDamage, Is.EqualTo(150));
Assert.That(highHealthHit.FinalDamage, Is.EqualTo(100));
@@ -63,8 +63,8 @@ namespace GeometryTD.Tests.EditMode
}
};
HitContext slowedHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 100, 100, true);
HitContext normalHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 100, 100, false);
HitContext slowedHit = TagEffectResolver.ResolveBeforeHit(CreateHitContext(attackPayload, 100, 100, 0.6f, new[] { TagType.Ice }));
HitContext normalHit = TagEffectResolver.ResolveBeforeHit(CreateHitContext(attackPayload, 100, 100));
Assert.That(slowedHit.FinalDamage, Is.EqualTo(150));
Assert.That(normalHit.FinalDamage, Is.EqualTo(100));
@@ -85,7 +85,7 @@ namespace GeometryTD.Tests.EditMode
}
};
HitContext hitContext = TagEffectResolver.ResolveBeforeHit(attackPayload, 30, 100, true, 0.05f);
HitContext hitContext = TagEffectResolver.ResolveBeforeHit(CreateHitContext(attackPayload, 30, 100, 0.6f, new[] { TagType.Ice }, 0.05f));
Assert.That(hitContext.IsCriticalHit, Is.True);
Assert.That(hitContext.FinalDamage, Is.EqualTo(281));
@@ -125,6 +125,63 @@ namespace GeometryTD.Tests.EditMode
Assert.That(stack, Is.EqualTo(2));
}
[Test]
public void ResolveBeforeHit_Uses_Context_TargetHealth_And_CritRoll()
{
AttackPayload attackPayload = new AttackPayload
{
BaseDamage = 100,
TagRuntimes = new[]
{
new TagRuntimeData { TagType = TagType.Crit, TotalStack = 1 },
new TagRuntimeData { TagType = TagType.Execution, TotalStack = 1 }
}
};
HitContext hitContext = TagEffectResolver.ResolveBeforeHit(CreateHitContext(attackPayload, 30, 100, 1f, null, 0.05f));
Assert.That(hitContext.IsCriticalHit, Is.True);
Assert.That(hitContext.FinalDamage, Is.EqualTo(225));
Assert.That(hitContext.IsKilled, Is.True);
}
[Test]
public void ResolveBeforeHit_Shatter_Uses_Context_SlowSnapshot()
{
AttackPayload attackPayload = new AttackPayload
{
BaseDamage = 100,
TagRuntimes = new[]
{
new TagRuntimeData { TagType = TagType.Shatter, TotalStack = 1 }
}
};
HitContext hitContext = TagEffectResolver.ResolveBeforeHit(CreateHitContext(attackPayload, 100, 100, 0.8f, new[] { TagType.Ice }));
Assert.That(hitContext.FinalDamage, Is.EqualTo(125));
}
private static HitContext CreateHitContext(
AttackPayload attackPayload,
int targetCurrentHealthBeforeHit,
int targetMaxHealth,
float targetMoveSpeedMultiplierBeforeHit = 1f,
TagType[] targetStatusTagsBeforeHit = null,
float? critRoll = null)
{
return new HitContext
{
AttackPayload = attackPayload,
TargetCurrentHealthBeforeHit = targetCurrentHealthBeforeHit,
TargetCurrentHealthAfterHit = targetCurrentHealthBeforeHit,
TargetMaxHealth = targetMaxHealth,
TargetMoveSpeedMultiplierBeforeHit = targetMoveSpeedMultiplierBeforeHit,
TargetStatusTagsBeforeHit = targetStatusTagsBeforeHit ?? System.Array.Empty<TagType>(),
CritRoll = critRoll
};
}
}
public sealed class EnemyTagStatusRuntimeTests
@@ -143,7 +200,7 @@ namespace GeometryTD.Tests.EditMode
};
int totalDamage = 0;
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
Assert.That(runtime.HasStatus(TagType.Fire), Is.True);
runtime.Tick(1f, damage => totalDamage += damage);
@@ -154,6 +211,32 @@ namespace GeometryTD.Tests.EditMode
Assert.That(totalDamage, Is.EqualTo(120));
}
[Test]
public void ApplyAfterHit_Fire_MaxEffectiveStack_Clamps_BaseFireStack_Only()
{
DRTagConfig fireRow = new DRTagConfig();
Assert.That(fireRow.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t火焰测试描述\t{\"BurnDurationSeconds\":3,\"BurnDamagePerSecondPerStack\":20,\"MaxEffectiveStack\":2}", null), Is.True);
TagDefinitionRegistry.LoadFromRows(new[] { fireRow });
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
AttackPayload attackPayload = new AttackPayload
{
BaseDamage = 100,
TagRuntimes = new[]
{
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 4 }
}
};
int totalDamage = 0;
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
runtime.Tick(3f, damage => totalDamage += damage);
Assert.That(totalDamage, Is.EqualTo(120));
TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void ApplyAfterHit_Fire_UsesElapsedDeltaTimeInsteadOfIndependentInterval()
{
@@ -168,7 +251,7 @@ namespace GeometryTD.Tests.EditMode
};
int totalDamage = 0;
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
runtime.Tick(0.5f, damage => totalDamage += damage);
runtime.Tick(0.5f, damage => totalDamage += damage);
@@ -191,7 +274,7 @@ namespace GeometryTD.Tests.EditMode
}
};
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
Assert.That(runtime.HasStatus(TagType.Ice), Is.True);
Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(0.6f).Within(0.001f));
@@ -217,7 +300,7 @@ namespace GeometryTD.Tests.EditMode
};
int totalDamage = 0;
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
Assert.That(runtime.HasStatus(TagType.Fire), Is.True);
Assert.That(runtime.HasStatus(TagType.Inferno), Is.False);
@@ -231,6 +314,35 @@ namespace GeometryTD.Tests.EditMode
Assert.That(runtime.HasStatus(TagType.Fire), Is.False);
}
[Test]
public void ApplyAfterHit_FireWithInferno_StillAppliesInfernoBonusAfterFireCap()
{
DRTagConfig fireRow = new DRTagConfig();
DRTagConfig infernoRow = new DRTagConfig();
Assert.That(fireRow.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t火焰测试描述\t{\"BurnDurationSeconds\":3,\"BurnDamagePerSecondPerStack\":20,\"MaxEffectiveStack\":2}", null), Is.True);
Assert.That(infernoRow.ParseDataRow("\t4\t元素\tInferno\tOnAfterHit\t强化燃烧伤害或持续时间\t{\"BonusBurnDurationSeconds\":0.5,\"BonusBurnDamagePerSecondPerStack\":10}", null), Is.True);
TagDefinitionRegistry.LoadFromRows(new[] { fireRow, infernoRow });
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
AttackPayload attackPayload = new AttackPayload
{
BaseDamage = 100,
TagRuntimes = new[]
{
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 4 },
new TagRuntimeData { TagType = TagType.Inferno, TotalStack = 1 }
}
};
int totalDamage = 0;
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
runtime.Tick(3.5f, damage => totalDamage += damage);
Assert.That(totalDamage, Is.EqualTo(175));
TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void ApplyAfterHit_InfernoWithoutFire_DoesNotCreateStatus()
{
@@ -244,12 +356,41 @@ namespace GeometryTD.Tests.EditMode
}
};
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
Assert.That(runtime.HasStatus(TagType.Fire), Is.False);
Assert.That(runtime.HasStatus(TagType.Inferno), Is.False);
}
[Test]
public void ApplyAfterHit_StatusModifier_Uses_Independent_OnAfterHit_Route()
{
DRTagConfig infernoRow = new DRTagConfig();
Assert.That(infernoRow.ParseDataRow("\t4\t元素\tInferno\tOnHit\t强化燃烧伤害或持续时间\t{\"BonusBurnDurationSeconds\":1,\"BonusBurnDamagePerSecondPerStack\":20}", null), Is.True);
TagDefinitionRegistry.LoadFromRows(new[] { infernoRow });
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
HitContext hitContext = CreateAfterHitContext(new AttackPayload
{
BaseDamage = 100,
TagRuntimes = new[]
{
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 1 },
new TagRuntimeData { TagType = TagType.Inferno, TotalStack = 1 }
}
}, runtime);
int totalDamage = 0;
TagEffectResolver.ApplyAfterHit(hitContext);
runtime.Tick(3f, damage => totalDamage += damage);
Assert.That(totalDamage, Is.EqualTo(60));
Assert.That(hitContext.StatusModifierContext.BonusBurnDamagePerSecond, Is.EqualTo(0f).Within(0.001f));
Assert.That(hitContext.StatusModifierContext.BonusBurnDurationSeconds, Is.EqualTo(0f).Within(0.001f));
TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void ApplyAfterHit_IceWithAbsoluteZero_IncreasesSlowStrengthAndDuration()
{
@@ -264,7 +405,7 @@ namespace GeometryTD.Tests.EditMode
}
};
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
Assert.That(runtime.HasStatus(TagType.Ice), Is.True);
Assert.That(runtime.HasStatus(TagType.AbsoluteZero), Is.False);
@@ -291,7 +432,7 @@ namespace GeometryTD.Tests.EditMode
}
};
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(attackPayload, runtime));
Assert.That(runtime.HasStatus(TagType.Ice), Is.False);
Assert.That(runtime.HasStatus(TagType.AbsoluteZero), Is.False);
@@ -323,12 +464,12 @@ namespace GeometryTD.Tests.EditMode
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
int totalDamage = 0;
TagEffectResolver.ApplyAfterHit(new AttackPayload
TagEffectResolver.ApplyAfterHit(CreateAfterHitContext(new AttackPayload
{
BaseDamage = tower.Stats.AttackDamage[0],
AttackPropertyType = tower.Stats.AttackPropertyType,
TagRuntimes = tower.Stats.TagRuntimes
}, runtime);
}, runtime));
runtime.Tick(1f, damage => totalDamage += damage);
runtime.Tick(1f, damage => totalDamage += damage);
@@ -337,6 +478,35 @@ namespace GeometryTD.Tests.EditMode
Assert.That(runtime.HasStatus(TagType.Fire), Is.False);
Assert.That(totalDamage, Is.EqualTo(180));
}
[Test]
public void ApplyAfterHit_Uses_TargetStatusRuntime_From_HitContext()
{
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
HitContext hitContext = CreateAfterHitContext(new AttackPayload
{
BaseDamage = 100,
TagRuntimes = new[]
{
new TagRuntimeData { TagType = TagType.Ice, TotalStack = 1 }
}
}, runtime);
TagEffectResolver.ApplyAfterHit(hitContext);
Assert.That(runtime.HasStatus(TagType.Ice), Is.True);
Assert.That(hitContext.TargetStatusRuntime, Is.SameAs(runtime));
}
private static HitContext CreateAfterHitContext(AttackPayload attackPayload, EnemyTagStatusRuntime runtime)
{
return new HitContext
{
AttackPayload = attackPayload,
TargetStatusRuntime = runtime,
TargetStatusTagsBeforeHit = System.Array.Empty<TagType>()
};
}
}
public sealed class EnemyStatusTagRegistryTests
@@ -372,8 +542,8 @@ namespace GeometryTD.Tests.EditMode
AssertDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit);
AssertDefinition(TagType.Inferno, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit);
AssertDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit);
AssertDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.OnHit);
AssertDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.OnKill);
AssertDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.None);
AssertDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.None);
AssertDefinition(TagType.AbsoluteZero, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit);
}
@@ -519,10 +689,21 @@ namespace GeometryTD.Tests.EditMode
};
Assert.DoesNotThrow(() => TagEffectResolver.ApplyOnHit(hitContext));
Assert.DoesNotThrow(() => TagEffectResolver.ApplyAfterHit(hitContext.AttackPayload, new EnemyTagStatusRuntime()));
hitContext.TargetStatusRuntime = new EnemyTagStatusRuntime();
Assert.DoesNotThrow(() => TagEffectResolver.ApplyAfterHit(hitContext));
Assert.DoesNotThrow(() => TagEffectResolver.ApplyOnKill(hitContext));
}
[Test]
public void Placeholder_AttackShape_Configs_Do_Not_Expose_Formal_Runtime_Params()
{
Assert.That(typeof(BurnSpreadTagConfig).GetProperty("SpreadRadius"), Is.Null);
Assert.That(typeof(IgniteBurstTagConfig).GetProperty("BurstRadius"), Is.Null);
Assert.That(typeof(FreezeMaskTagConfig).GetProperty("FreezeBuildUpPerStack"), Is.Null);
Assert.That(typeof(PierceTagConfig).GetProperty("ExtraPierceCount"), Is.Null);
Assert.That(typeof(OverpenetrateTagConfig).GetProperty("RemainingDamageRate"), Is.Null);
}
private static void AssertDefinition(TagType tagType, TagCategory expectedCategory, TagTriggerPhase expectedPhase)
{
Assert.That(TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition), Is.True);
@@ -577,6 +758,9 @@ namespace GeometryTD.Tests.EditMode
{
BaseDamage = 80,
AttackPropertyType = AttackPropertyType.Fire,
SourceEntityId = 101,
ProjectileEntityId = 202,
OriginPosition = new Vector3(1f, 2f, 0f),
TagRuntimes = new[]
{
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 1 }
@@ -590,6 +774,9 @@ namespace GeometryTD.Tests.EditMode
Assert.That(receiver.LastPayload, Is.Not.Null);
Assert.That(receiver.LastPayload.BaseDamage, Is.EqualTo(80));
Assert.That(receiver.LastPayload.AttackPropertyType, Is.EqualTo(AttackPropertyType.Fire));
Assert.That(receiver.LastPayload.SourceEntityId, Is.EqualTo(101));
Assert.That(receiver.LastPayload.ProjectileEntityId, Is.EqualTo(202));
Assert.That(receiver.LastPayload.OriginPosition, Is.EqualTo(new Vector3(1f, 2f, 0f)));
Assert.That(receiver.LastPayload.TagRuntimes, Has.Length.EqualTo(1));
Assert.That(receiver.LastPayload.TagRuntimes[0].TagType, Is.EqualTo(TagType.Fire));
}
@@ -600,6 +787,30 @@ namespace GeometryTD.Tests.EditMode
}
}
[Test]
public void AttackPayload_Clone_Preserves_Source_Metadata()
{
AttackPayload payload = new AttackPayload
{
BaseDamage = 50,
AttackPropertyType = AttackPropertyType.Physics,
SourceEntityId = 11,
ProjectileEntityId = 22,
OriginPosition = new Vector3(3f, 4f, 0f),
TagRuntimes = new[]
{
new TagRuntimeData { TagType = TagType.Crit, TotalStack = 1 }
}
};
AttackPayload cloned = payload.Clone();
Assert.That(cloned.SourceEntityId, Is.EqualTo(11));
Assert.That(cloned.ProjectileEntityId, Is.EqualTo(22));
Assert.That(cloned.OriginPosition, Is.EqualTo(new Vector3(3f, 4f, 0f)));
Assert.That(cloned.TagRuntimes, Is.Not.SameAs(payload.TagRuntimes));
}
private sealed class RecordingDamageReceiver : MonoBehaviour, IDamageReceiver
{
public AttackPayload LastPayload { get; private set; }