Vampire-Act-Base/tests/Nightborn.Core.Tests/CombatLogicTests.cs

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));
}
}