Start Tag System
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
using Components;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Tests.EditMode
|
||||
{
|
||||
public sealed class TagEffectResolverTests
|
||||
{
|
||||
[Test]
|
||||
public void ResolveBeforeHit_Crit_CanIncreaseDamage()
|
||||
{
|
||||
AttackPayload attackPayload = new AttackPayload
|
||||
{
|
||||
BaseDamage = 100,
|
||||
AttackPropertyType = AttackPropertyType.Physics,
|
||||
TagRuntimes = new[]
|
||||
{
|
||||
new TagRuntimeData { TagType = TagType.Crit, TotalStack = 1 }
|
||||
}
|
||||
};
|
||||
|
||||
HitContext hitContext = TagEffectResolver.ResolveBeforeHit(attackPayload, 100, 100, false, 0.05f);
|
||||
|
||||
Assert.That(hitContext.IsCriticalHit, Is.True);
|
||||
Assert.That(hitContext.FinalDamage, Is.EqualTo(150));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResolveBeforeHit_Execution_OnlyTriggersOnLowHealthTarget()
|
||||
{
|
||||
AttackPayload attackPayload = new AttackPayload
|
||||
{
|
||||
BaseDamage = 100,
|
||||
AttackPropertyType = AttackPropertyType.Physics,
|
||||
TagRuntimes = new[]
|
||||
{
|
||||
new TagRuntimeData { TagType = TagType.Execution, TotalStack = 1 }
|
||||
}
|
||||
};
|
||||
|
||||
HitContext lowHealthHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 30, 100, false);
|
||||
HitContext highHealthHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 31, 100, false);
|
||||
|
||||
Assert.That(lowHealthHit.FinalDamage, Is.EqualTo(150));
|
||||
Assert.That(highHealthHit.FinalDamage, Is.EqualTo(100));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EnemyTagStatusRuntimeTests
|
||||
{
|
||||
[Test]
|
||||
public void ApplyAfterHit_Fire_AddsBurnDamageOverTime()
|
||||
{
|
||||
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
|
||||
AttackPayload attackPayload = new AttackPayload
|
||||
{
|
||||
BaseDamage = 100,
|
||||
TagRuntimes = new[]
|
||||
{
|
||||
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 2 }
|
||||
}
|
||||
};
|
||||
int totalDamage = 0;
|
||||
|
||||
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
|
||||
Assert.That(runtime.HasStatus(TagType.Fire), Is.True);
|
||||
|
||||
runtime.Tick(1f, damage => totalDamage += damage);
|
||||
runtime.Tick(1f, damage => totalDamage += damage);
|
||||
runtime.Tick(1f, damage => totalDamage += damage);
|
||||
|
||||
Assert.That(runtime.HasStatus(TagType.Fire), Is.False);
|
||||
Assert.That(totalDamage, Is.EqualTo(120));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyAfterHit_Fire_UsesElapsedDeltaTimeInsteadOfIndependentInterval()
|
||||
{
|
||||
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
|
||||
AttackPayload attackPayload = new AttackPayload
|
||||
{
|
||||
BaseDamage = 100,
|
||||
TagRuntimes = new[]
|
||||
{
|
||||
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 1 }
|
||||
}
|
||||
};
|
||||
int totalDamage = 0;
|
||||
|
||||
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
|
||||
|
||||
runtime.Tick(0.5f, damage => totalDamage += damage);
|
||||
runtime.Tick(0.5f, damage => totalDamage += damage);
|
||||
runtime.Tick(2f, damage => totalDamage += damage);
|
||||
|
||||
Assert.That(totalDamage, Is.EqualTo(60));
|
||||
Assert.That(runtime.HasStatus(TagType.Fire), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyAfterHit_Ice_AddsSlowAndRecoversAfterDuration()
|
||||
{
|
||||
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
|
||||
AttackPayload attackPayload = new AttackPayload
|
||||
{
|
||||
BaseDamage = 100,
|
||||
TagRuntimes = new[]
|
||||
{
|
||||
new TagRuntimeData { TagType = TagType.Ice, TotalStack = 2 }
|
||||
}
|
||||
};
|
||||
|
||||
TagEffectResolver.ApplyAfterHit(attackPayload, runtime);
|
||||
|
||||
Assert.That(runtime.HasStatus(TagType.Ice), Is.True);
|
||||
Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(0.6f).Within(0.001f));
|
||||
|
||||
runtime.Tick(2f, null);
|
||||
|
||||
Assert.That(runtime.HasStatus(TagType.Ice), Is.False);
|
||||
Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(1f).Within(0.001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Tick_OnlyUpdatesActivatedStatusTags()
|
||||
{
|
||||
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
|
||||
int totalDamage = 0;
|
||||
|
||||
runtime.Tick(5f, damage => totalDamage += damage);
|
||||
|
||||
Assert.That(totalDamage, Is.EqualTo(0));
|
||||
Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(1f).Within(0.001f));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EnemyStatusTagRegistryTests
|
||||
{
|
||||
[Test]
|
||||
public void Registry_Registers_All_Status_Tags()
|
||||
{
|
||||
Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Fire, out IEnemyStatusTagEffect fireEffect), Is.True);
|
||||
Assert.That(fireEffect, Is.TypeOf<FireTagEffect>());
|
||||
|
||||
Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Ice, out IEnemyStatusTagEffect iceEffect), Is.True);
|
||||
Assert.That(iceEffect, Is.TypeOf<IceTagEffect>());
|
||||
|
||||
Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Inferno, out IEnemyStatusTagEffect infernoEffect), Is.True);
|
||||
Assert.That(infernoEffect, Is.TypeOf<InfernoTagEffect>());
|
||||
|
||||
Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.AbsoluteZero, out IEnemyStatusTagEffect absoluteZeroEffect), Is.True);
|
||||
Assert.That(absoluteZeroEffect, Is.TypeOf<AbsoluteZeroTagEffect>());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TagConfigRegistryTests
|
||||
{
|
||||
[Test]
|
||||
public void Definitions_Register_All_Twelve_Tags()
|
||||
{
|
||||
Assert.That(TagConfigRegistry.Definitions.Count, Is.EqualTo(12));
|
||||
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Fire, out _), Is.True);
|
||||
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Execution, out _), Is.True);
|
||||
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Overpenetrate, out _), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Definitions_Assign_Documented_Categories_And_Phases()
|
||||
{
|
||||
AssertDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit);
|
||||
AssertDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit);
|
||||
AssertDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.OnHit);
|
||||
AssertDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.OnKill);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Definitions_Keep_Unimplemented_Tags_As_Explicit_NoOpPlaceholders()
|
||||
{
|
||||
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(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(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 AttackShape_Placeholders_Are_Routable_Without_Runtime_Effect()
|
||||
{
|
||||
HitContext hitContext = new HitContext
|
||||
{
|
||||
AttackPayload = new AttackPayload
|
||||
{
|
||||
BaseDamage = 100,
|
||||
TagRuntimes = new[]
|
||||
{
|
||||
new TagRuntimeData { TagType = TagType.Pierce, TotalStack = 1 },
|
||||
new TagRuntimeData { TagType = TagType.BurnSpread, TotalStack = 1 },
|
||||
new TagRuntimeData { TagType = TagType.FreezeMask, TotalStack = 1 }
|
||||
}
|
||||
},
|
||||
FinalDamage = 100
|
||||
};
|
||||
|
||||
Assert.DoesNotThrow(() => TagEffectResolver.ApplyOnHit(hitContext));
|
||||
Assert.DoesNotThrow(() => TagEffectResolver.ApplyAfterHit(hitContext.AttackPayload, new EnemyTagStatusRuntime()));
|
||||
Assert.DoesNotThrow(() => TagEffectResolver.ApplyOnKill(hitContext));
|
||||
}
|
||||
|
||||
private static void AssertDefinition(TagType tagType, TagCategory expectedCategory, TagTriggerPhase expectedPhase)
|
||||
{
|
||||
Assert.That(TagConfigRegistry.TryGetDefinition(tagType, out TagDefinitionData definition), Is.True);
|
||||
Assert.That(definition, Is.Not.Null);
|
||||
Assert.That(definition.Category, Is.EqualTo(expectedCategory));
|
||||
Assert.That(definition.TriggerPhase, Is.EqualTo(expectedPhase));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AttackPayloadFlowTests
|
||||
{
|
||||
[Test]
|
||||
public void ShooterBullet_PassesAttackPayloadToDamageReceiver()
|
||||
{
|
||||
GameObject bulletObject = new GameObject("Bullet");
|
||||
GameObject targetObject = new GameObject("Target");
|
||||
try
|
||||
{
|
||||
ShooterBullet shooterBullet = bulletObject.AddComponent<ShooterBullet>();
|
||||
RecordingDamageReceiver receiver = targetObject.AddComponent<RecordingDamageReceiver>();
|
||||
targetObject.transform.position = new Vector3(0.01f, 0f, 0f);
|
||||
|
||||
AttackPayload payload = new AttackPayload
|
||||
{
|
||||
BaseDamage = 80,
|
||||
AttackPropertyType = AttackPropertyType.Fire,
|
||||
TagRuntimes = new[]
|
||||
{
|
||||
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 1 }
|
||||
}
|
||||
};
|
||||
BulletData bulletData = new BulletData(1, 501, Vector3.zero, targetObject.transform, payload, 12f);
|
||||
|
||||
shooterBullet.OnShow(bulletData);
|
||||
shooterBullet.Tick(0.02f);
|
||||
|
||||
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.TagRuntimes, Has.Length.EqualTo(1));
|
||||
Assert.That(receiver.LastPayload.TagRuntimes[0].TagType, Is.EqualTo(TagType.Fire));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(bulletObject);
|
||||
Object.DestroyImmediate(targetObject);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingDamageReceiver : MonoBehaviour, IDamageReceiver
|
||||
{
|
||||
public AttackPayload LastPayload { get; private set; }
|
||||
|
||||
public void TakeDamage(AttackPayload attackPayload)
|
||||
{
|
||||
LastPayload = attackPayload?.Clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86fbd79741310d04bae44ad6ec103c84
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user