using System; using System.Collections.Generic; using System.Reflection; using BumpCombat.Combat; using BumpCombat.Constants; using BumpCombat.Core; using BumpCombat.Enemies; using BumpCombat.Player; using NUnit.Framework; using UnityEngine; namespace BumpCombat.Tests { public sealed class ArtifactGroggyEditModeRegressionTests { private static readonly BindingFlags NonPublicInstance = BindingFlags.Instance | BindingFlags.NonPublic; private readonly List createdObjects = new(); private GameObject owner; [SetUp] public void SetUp() { owner = new GameObject("Artifact Groggy EditMode Tests"); } [TearDown] public void TearDown() { if (owner != null) { UnityEngine.Object.DestroyImmediate(owner); } for (int i = createdObjects.Count - 1; i >= 0; i--) { if (createdObjects[i] != null) { UnityEngine.Object.DestroyImmediate(createdObjects[i]); } } createdObjects.Clear(); } [Test] public void EnemyKindInfersGroggyTierAndContactRequirements() { EnemyController elite = CreateEnemy("Elite", EnemyKind.ArmoredSkeleton); EnemyController midBoss = CreateEnemy( "Mid Boss", EnemyKind.GreatswordSkeleton); EnemyController finalBoss = CreateEnemy( "Final Boss", EnemyKind.Necromancer); Assert.That(elite.GroggyTier, Is.EqualTo(RunTimedEvent.Elite)); Assert.That(elite.ShieldHitRequirement, Is.EqualTo(1)); Assert.That(elite.GroggyRequiredContactCount, Is.EqualTo(1)); Assert.That(midBoss.GroggyTier, Is.EqualTo(RunTimedEvent.MidBoss)); Assert.That(midBoss.ShieldHitRequirement, Is.EqualTo(2)); Assert.That(midBoss.GroggyRequiredContactCount, Is.EqualTo(2)); Assert.That(finalBoss.GroggyTier, Is.EqualTo(RunTimedEvent.FinalBoss)); Assert.That(finalBoss.ShieldHitRequirement, Is.EqualTo(4)); Assert.That(finalBoss.GroggyRequiredContactCount, Is.EqualTo(4)); } [Test] public void EliteGroggyRequirement_ReadsChangedEnemySettings() { EnemyConstants constants = GameplayConstants.Current.Enemies; int originalRequirement = constants.EliteGroggyRequiredMatchingHits; try { constants.EliteGroggyRequiredMatchingHits = 3; EnemyController enemy = CreateEnemy( "Elite Changed Threshold", EnemyKind.ArmoredSkeleton); Assert.That(enemy.ShieldHitRequirement, Is.EqualTo(3)); Assert.That(enemy.GroggyRequiredContactCount, Is.EqualTo(3)); } finally { constants.EliteGroggyRequiredMatchingHits = originalRequirement; } } [Test] public void NormalEnemyRemainsDamageableWithoutGroggyShield() { EnemyController enemy = CreateEnemy("Normal", EnemyKind.Skeleton); float healthBefore = enemy.CurrentHealth; Assert.That(enemy.IsGroggyTierGated, Is.False); Assert.That(enemy.IsDamageInvulnerable, Is.False); Assert.That( enemy.TryTakeDamage(5f, Vector2.right, 0f, false), Is.True); Assert.That(enemy.CurrentHealth, Is.EqualTo(healthBefore - 5f)); Assert.That( enemy.TryTakeArtifactHit( 7f, Vector2.right, 0f, ArtifactColor.Blue, 1, false), Is.EqualTo(EnemyArtifactHitResult.DamageApplied)); } [Test] public void GroggyShieldBlocksNormalArtifactAndDotDamageBeforeOpening() { EnemyController enemy = CreateEnemy("Elite Gate", EnemyKind.Werewolf); float healthBefore = enemy.CurrentHealth; Assert.That(enemy.IsDamageInvulnerable, Is.True); Assert.That( enemy.TryTakeDamage(5f, Vector2.right, 0f, false), Is.False); Assert.That(enemy.CurrentHealth, Is.EqualTo(healthBefore)); Assert.That(enemy.ApplyIgnite(1f, 0.5f, 4f), Is.True); InvokeTimedEffects(enemy, 0.5f); Assert.That( enemy.CurrentHealth, Is.EqualTo(healthBefore), "A DOT tick must not bypass the groggy shield."); Assert.That(enemy.GroggyQualifyingContactCount, Is.Zero); Assert.That( enemy.TryTakeArtifactHit( 9f, Vector2.right, 0f, ArtifactColor.Red, 1, false), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(enemy.LastAppliedDamage, Is.Zero); Assert.That(enemy.CurrentHealth, Is.EqualTo(healthBefore)); Assert.That(enemy.IsGroggy, Is.False); Assert.That( enemy.TryTakeArtifactHit( 9f, Vector2.right, 0f, ArtifactColor.Green, 2, false), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(enemy.IsGroggy, Is.True); Assert.That(enemy.IsDamageInvulnerable, Is.False); Assert.That( enemy.TryTakeDamage(5f, Vector2.right, 0f, false), Is.True); Assert.That( enemy.TryTakeArtifactHit( 7f, Vector2.right, 0f, ArtifactColor.Blue, 3, false), Is.EqualTo(EnemyArtifactHitResult.DamageApplied)); float healthAfterDirectHits = enemy.CurrentHealth; InvokeTimedEffects(enemy, 0.5f); Assert.That( enemy.CurrentHealth, Is.LessThan(healthAfterDirectHits), "The same DOT API is allowed to tick after groggy opens."); } [Test] public void MidBossNeedsTwoMatchingColorContacts() { EnemyController enemy = CreateEnemy( "Mid Boss Threshold", EnemyKind.NecroGolem); Assert.That(Contact(enemy, ArtifactColor.Green, 11), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(enemy.IsGroggy, Is.False); Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(1)); Assert.That( Contact(enemy, ArtifactColor.Green, 12), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(enemy.IsGroggy, Is.True); Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(2)); Assert.That(enemy.IsDamageInvulnerable, Is.False); } [Test] public void FinalBossRequiresFourMatchingColorContacts() { EnemyController enemy = CreateEnemy( "Final Boss Four Singles", EnemyKind.Necromancer); Assert.That(Contact(enemy, ArtifactColor.Red, 21), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(Contact(enemy, ArtifactColor.Blue, 22), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(Contact(enemy, ArtifactColor.Blue, 23), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(Contact(enemy, ArtifactColor.Red, 24), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(enemy.IsGroggy, Is.False); Assert.That(enemy.IsDamageInvulnerable, Is.True); Assert.That(enemy.ShieldHitsRemaining, Is.EqualTo(4)); Assert.That(enemy.GroggyQualifyingContactCount, Is.Zero); Assert.That(Contact(enemy, ArtifactColor.Green, 25), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(Contact(enemy, ArtifactColor.Green, 26), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(Contact(enemy, ArtifactColor.Green, 27), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(Contact(enemy, ArtifactColor.Green, 28), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(enemy.IsGroggy, Is.True); } [Test] public void FinalBossFourCastsOfOneColorOpenGroggy() { EnemyController enemy = CreateEnemy( "Final Boss Four Green Activations", EnemyKind.Necromancer); Assert.That(Contact(enemy, ArtifactColor.Green, 31), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(Contact(enemy, ArtifactColor.Green, 32), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(Contact(enemy, ArtifactColor.Green, 33), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That( Contact(enemy, ArtifactColor.Green, 34), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(enemy.IsGroggy, Is.True); Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(4)); } [Test] public void SameCastMultiTickIsDeduplicatedPerEnemy() { EnemyController enemy = CreateEnemy( "Mid Boss Dedup", EnemyKind.GreatswordSkeleton); Assert.That(Contact(enemy, ArtifactColor.Green, 41), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That( Contact(enemy, ArtifactColor.Green, 41), Is.EqualTo(EnemyArtifactHitResult.Rejected)); Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(1)); Assert.That(enemy.IsGroggy, Is.False); Assert.That(Contact(enemy, ArtifactColor.Green, 42), Is.EqualTo(EnemyArtifactHitResult.Shielded)); Assert.That(enemy.IsGroggy, Is.True); } [Test] public void GroggyHitsDoNotExtendOrPrechargeTheNextCycle() { EnemyController enemy = CreateEnemy("Groggy Rearm", EnemyKind.Werebear); Assert.That(Contact(enemy, ArtifactColor.Green, 51), Is.EqualTo(EnemyArtifactHitResult.Shielded)); float remaining = enemy.GroggyTimeRemaining; Assert.That( Contact(enemy, ArtifactColor.Blue, 52), Is.EqualTo(EnemyArtifactHitResult.DamageApplied)); Assert.That(enemy.GroggyTimeRemaining, Is.EqualTo(remaining).Within(0.001f)); Assert.That(enemy.ApplyStun(2f), Is.False); Assert.That(enemy.IsStunned, Is.False); enemy.EndGroggyForProtection(); Assert.That(enemy.IsGroggy, Is.False); Assert.That(enemy.IsDamageInvulnerable, Is.True); Assert.That(enemy.GroggyQualifyingContactCount, Is.Zero); Assert.That( enemy.RegisterArtifactContact(ArtifactColor.Blue, 52), Is.EqualTo(EnemyArtifactContactResult.Ignored), "A cast that first contacted during groggy must not precharge the next shield."); Assert.That( enemy.RegisterArtifactContact(ArtifactColor.Red, 53), Is.EqualTo(EnemyArtifactContactResult.Shielded)); Assert.That(enemy.IsGroggy, Is.True); Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(1)); } private EnemyController CreateEnemy(string name, EnemyKind kind) { EnemyDefinition definition = ScriptableObject.CreateInstance(); definition.Configure( kind, EnemyAttackShape.Body, 100f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0); createdObjects.Add(definition); GameObject enemyObject = new(name); enemyObject.transform.SetParent(owner.transform); enemyObject.AddComponent(); enemyObject.AddComponent(); enemyObject.AddComponent(); EnemyController enemy = enemyObject.AddComponent(); enemy.Configure(definition, null); EnemyAttack enemyAttack = enemyObject.GetComponent(); enemyAttack.GetType() .GetMethod("Awake", NonPublicInstance) ?.Invoke(enemyAttack, null); enemy.GetType() .GetMethod("Awake", NonPublicInstance) ?.Invoke(enemy, null); SetAutoProperty(enemy, "CurrentHealth", 100f); return enemy; } private static EnemyArtifactHitResult Contact( EnemyController enemy, ArtifactColor artifactColor, int castIdentity) { return enemy.TryTakeArtifactHit( 10f, Vector2.right, 0f, artifactColor, castIdentity, false); } private static void InvokeTimedEffects( EnemyController enemy, float deltaTime) { MethodInfo method = typeof(EnemyController).GetMethod( "UpdateTimedEffects", NonPublicInstance); Assert.That(method, Is.Not.Null); method.Invoke(enemy, new object[] { deltaTime }); } private static void SetAutoProperty( object target, string propertyName, object value) { FieldInfo field = target.GetType().GetField( $"<{propertyName}>k__BackingField", NonPublicInstance); Assert.That(field, Is.Not.Null, $"Missing {propertyName} backing field."); field.SetValue(target, value); } } }