using System.Collections.Generic; using Nightborn.Core.Combat; using Nightborn.Core.Geometry; namespace Nightborn.Core.Tests; public class CombatLogicTests { private sealed class FixedRandomProvider : IRandomProvider { private readonly Queue _values; public FixedRandomProvider(params float[] values) { _values = new Queue(values); } public float NextFloat() { if (_values.Count == 0) { return 0.99f; } return _values.Dequeue(); } } [Test] public void ResolveAttacks_HumanSectorHit_ShouldEmitHitWithHumanForm() { var random = new FixedRandomProvider(0.9f); var sut = new CombatLogic(random); var lastForm = FormType.Mist; sut.OnHit += r => lastForm = r.SourceForm; var attacks = new List { new AttackData { AttackId = 1, Origin = new Vector3(0f, 0f, 0f), HitShape = new Sector(new Vector3(0f, 0f, 0f), 3f, 90f, 0f), SourceForm = FormType.Human, BaseDamage = 10f, CritChance = 0f, CritMultiplier = 1.5f, KnockbackForce = 2f } }; var enemies = new List { new EnemyState(100, 100f, 2f, new Circle(new Vector3(2f, 0f, 0f), 0.5f)) }; var results = sut.ResolveAttacks(attacks, enemies, 1f / 60f); Assert.That(results.Count, Is.EqualTo(1)); Assert.That(lastForm, Is.EqualTo(FormType.Human)); } [Test] public void ResolveAttacks_WolfRectOutOfRange_ShouldMiss() { var sut = new CombatLogic(new FixedRandomProvider(0.9f)); var attacks = new List { new AttackData { AttackId = 1, HitShape = new Rect(new Vector3(0f, 0f, 0f), 1.5f, 5f, 0f), SourceForm = FormType.Wolf, BaseDamage = 20f, CritChance = 0f, CritMultiplier = 1.5f } }; var enemies = new List { new EnemyState(1, 100f, 1f, new Circle(new Vector3(6f, 0f, 0f), 0.4f)) }; var results = sut.ResolveAttacks(attacks, enemies, 1f / 60f); Assert.That(results, Is.Empty); } [Test] public void ResolveAttacks_MistCircle_ShouldHitAllEnemiesInRange() { var sut = new CombatLogic(new FixedRandomProvider(0.9f, 0.9f, 0.9f)); var attacks = new List { new AttackData { AttackId = 7, HitShape = new Circle(new Vector3(0f, 0f, 0f), 5f), SourceForm = FormType.Mist, BaseDamage = 8f, CritChance = 0f, CritMultiplier = 1.5f } }; var enemies = new List { new EnemyState(1, 10f, 1f, new Circle(new Vector3(1f, 0f, 1f), 0.5f)), new EnemyState(2, 10f, 1f, new Circle(new Vector3(2f, 0f, 2f), 0.5f)), new EnemyState(3, 10f, 1f, new Circle(new Vector3(4f, 0f, 0f), 0.5f)) }; var results = sut.ResolveAttacks(attacks, enemies, 1f / 60f); Assert.That(results.Count, Is.EqualTo(3)); } [Test] public void ResolveAttacks_CritChanceAboveOne_ShouldAlwaysCrit() { var sut = new CombatLogic(new FixedRandomProvider(0.5f)); var attacks = new List { new AttackData { AttackId = 1, HitShape = new Circle(new Vector3(0f, 0f, 0f), 2f), SourceForm = FormType.Mist, BaseDamage = 10f, CritChance = 1.2f, CritMultiplier = 2f } }; var enemies = new List { new EnemyState(1, 100f, 1f, new Circle(new Vector3(0.5f, 0f, 0f), 0.5f)) }; var result = sut.ResolveAttacks(attacks, enemies, 1f / 60f); Assert.That(result[0].IsCritical, Is.True); Assert.That(result[0].FinalDamage, Is.EqualTo(20f).Within(0.0001f)); } [Test] public void ResolveAttacks_HighDamageFirst_ShouldKillAndSkipLaterHits() { var sut = new CombatLogic(new FixedRandomProvider(0.9f, 0.9f)); var enemies = new List { new EnemyState(1, 10f, 1f, new Circle(new Vector3(0f, 0f, 0f), 1f)) }; var attacks = new List { new AttackData { AttackId = 11, HitShape = new Circle(new Vector3(0f, 0f, 0f), 2f), SourceForm = FormType.Human, BaseDamage = 8f, CritChance = 0f, CritMultiplier = 1.5f }, new AttackData { AttackId = 12, HitShape = new Circle(new Vector3(0f, 0f, 0f), 2f), SourceForm = FormType.Wolf, BaseDamage = 15f, CritChance = 0f, CritMultiplier = 1.5f } }; var results = sut.ResolveAttacks(attacks, enemies, 1f / 60f); Assert.That(results.Count, Is.EqualTo(1)); Assert.That(results[0].AttackId, Is.EqualTo(12)); Assert.That(results[0].IsKillingBlow, Is.True); } [Test] public void ResolveAttacks_DeltaTimeZero_ShouldReturnEmptyAndNoEvents() { var sut = new CombatLogic(new FixedRandomProvider(0.1f)); var eventCount = 0; sut.OnHit += _ => eventCount++; sut.OnKill += _ => eventCount++; sut.OnCrit += _ => eventCount++; sut.OnAttackResolved += (_, _) => eventCount++; var results = sut.ResolveAttacks( new List { new AttackData { AttackId = 1, HitShape = new Circle(new Vector3(0f, 0f, 0f), 2f), SourceForm = FormType.Mist, BaseDamage = 10f, CritChance = 0f, CritMultiplier = 1.5f } }, new List { new EnemyState(1, 10f, 1f, new Circle(new Vector3(0f, 0f, 0f), 0.5f)) }, 0f); Assert.That(results, Is.Empty); Assert.That(eventCount, Is.EqualTo(0)); } [Test] public void TestHit_TangentCircleCircle_ShouldBeMiss() { var a = new Circle(new Vector3(0f, 0f, 0f), 1f); var b = new Circle(new Vector3(2f, 0f, 0f), 1f); var hit = CombatLogic.TestHit(a, b); Assert.That(hit, Is.False); } [Test] public void ResolveAttacks_DuplicateTargetIdInList_ShouldDeduplicatePerAttack() { var sut = new CombatLogic(new FixedRandomProvider(0.9f)); var attack = new AttackData { AttackId = 1, HitShape = new Circle(new Vector3(0f, 0f, 0f), 3f), SourceForm = FormType.Mist, BaseDamage = 5f, CritChance = 0f, CritMultiplier = 1.5f }; var enemyA = new EnemyState(10, 20f, 1f, new Circle(new Vector3(0f, 0f, 0f), 1f)); var enemyB = new EnemyState(10, 20f, 1f, new Circle(new Vector3(0f, 0f, 0f), 1f)); var results = sut.ResolveAttacks(new List { attack }, new List { enemyA, enemyB }, 1f / 60f); Assert.That(results.Count, Is.EqualTo(1)); } [Test] public void CalculateKnockbackDistance_ShouldApplyCriticalMultiplier() { var normal = CombatLogic.CalculateKnockbackDistance(10f, 2f, false); var critical = CombatLogic.CalculateKnockbackDistance(10f, 2f, true); Assert.That(normal, Is.EqualTo(5f).Within(0.0001f)); Assert.That(critical, Is.EqualTo(7.5f).Within(0.0001f)); } [Test] public void ResolveAttacks_Miss_ShouldStillEmitAttackResolvedWithZero() { var sut = new CombatLogic(new FixedRandomProvider(0.9f)); var resolvedCount = -1; sut.OnAttackResolved += (_, hitCount) => resolvedCount = hitCount; var attacks = new List { new AttackData { AttackId = 2, HitShape = new Circle(new Vector3(0f, 0f, 0f), 1f), SourceForm = FormType.Mist, BaseDamage = 5f, CritChance = 0f, CritMultiplier = 1.5f } }; var enemies = new List { new EnemyState(1, 10f, 1f, new Circle(new Vector3(10f, 0f, 0f), 0.5f)) }; _ = sut.ResolveAttacks(attacks, enemies, 1f / 60f); Assert.That(resolvedCount, Is.EqualTo(0)); } }