281 lines
8.7 KiB
C#
281 lines
8.7 KiB
C#
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<float> _values;
|
|
|
|
public FixedRandomProvider(params float[] values)
|
|
{
|
|
_values = new Queue<float>(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<AttackData>
|
|
{
|
|
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<EnemyState>
|
|
{
|
|
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<AttackData>
|
|
{
|
|
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<EnemyState>
|
|
{
|
|
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<AttackData>
|
|
{
|
|
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<EnemyState>
|
|
{
|
|
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<AttackData>
|
|
{
|
|
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<EnemyState>
|
|
{
|
|
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<EnemyState>
|
|
{
|
|
new EnemyState(1, 10f, 1f, new Circle(new Vector3(0f, 0f, 0f), 1f))
|
|
};
|
|
|
|
var attacks = new List<AttackData>
|
|
{
|
|
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<AttackData>
|
|
{
|
|
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<EnemyState> { 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<AttackData> { attack }, new List<EnemyState> { 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<AttackData>
|
|
{
|
|
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<EnemyState>
|
|
{
|
|
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));
|
|
}
|
|
}
|