2.5 update
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using BumpCombat.Constants;
|
||||
using BumpCombat.Defense.Comparison;
|
||||
using BumpCombat.Enemies;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Tests
|
||||
{
|
||||
public sealed class Defense25DMonsterPoolCompositionTests
|
||||
{
|
||||
private static readonly string[][] SpeciesByTier =
|
||||
{
|
||||
new[] { "Slime", "Bat", "Skeleton", "Orc", "Hellbat", "Hellhound", "LavaSlime" },
|
||||
new[] { "ArmoredSkeleton", "SkeletonArcher", "Werewolf", "Werebear", "ArmoredOrc", "BloodMonster_A", "BloodMonster_B", "EyeballMonster", "Ghostfire" },
|
||||
new[] { "GreatswordSkeleton", "EliteOrc", "OrcRider", "Demon_A", "Demon_B", "Demon_C", "Demon_D", "Demon_E", "Demoness_A", "Demoness_B", "FlameGolem", "Minotaur" },
|
||||
};
|
||||
|
||||
private readonly List<UnityEngine.Object> created = new List<UnityEngine.Object>();
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (UnityEngine.Object item in created)
|
||||
if (item != null) UnityEngine.Object.DestroyImmediate(item);
|
||||
created.Clear();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExplicitCatalogAndTierBoundariesValidate()
|
||||
{
|
||||
Defense25DMonsterPoolDefinition pool = MakePool();
|
||||
Assert.That(pool.TryValidate(out string error), Is.True, error);
|
||||
Assert.That(pool.Tiers.Select(tier => tier.TierId), Is.EqualTo(new[] { 0, 1, 2 }));
|
||||
Assert.That(pool.Tiers.Select(tier => tier.FromCombatSeconds), Is.EqualTo(new[] { 0f, 180f, 360f }));
|
||||
Assert.That(pool.Tiers.SelectMany(tier => tier.Entries).Count(), Is.EqualTo(28));
|
||||
Assert.That(pool.TryGetEntry("Necromancer", out _), Is.False);
|
||||
|
||||
AssertTier(0f, 0);
|
||||
AssertTier(179.999f, 0);
|
||||
AssertTier(180f, 1);
|
||||
AssertTier(359.999f, 1);
|
||||
AssertTier(360f, 2);
|
||||
AssertInvalidTier(float.NaN);
|
||||
AssertInvalidTier(float.PositiveInfinity);
|
||||
AssertInvalidTier(-1f);
|
||||
}
|
||||
|
||||
[TestCase(-1)]
|
||||
[TestCase(3)]
|
||||
[TestCase(4)]
|
||||
[TestCase(5)]
|
||||
public void DirectorAdapterRejectsNonNormalizedTierIds(int tierId)
|
||||
{
|
||||
Defense25DMonsterPoolDefinition pool = MakePool();
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => pool.GetDirectorEntriesForTier(tierId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompositionUsesStableSeedNominalTierThreatBudgetAndBoundedOvershoot()
|
||||
{
|
||||
Defense25DMonsterPoolDefinition pool = MakePool();
|
||||
Defense25DWaveComposition first = pool.BuildWaveComposition(8, 179.999f, 11f, 417);
|
||||
Defense25DWaveComposition replay = pool.BuildWaveComposition(8, 179.999f, 11f, 417);
|
||||
Defense25DWaveComposition changedSeed = pool.BuildWaveComposition(8, 179.999f, 11f, 418);
|
||||
|
||||
Assert.That(first.TierId, Is.EqualTo(0));
|
||||
Assert.That(first.Seed, Is.EqualTo(417));
|
||||
Assert.That(first.EnemyDefinitionIds, Is.EqualTo(replay.EnemyDefinitionIds));
|
||||
Assert.That(first.ThreatCosts, Is.EqualTo(replay.ThreatCosts));
|
||||
Assert.That(first.EnemyDefinitionIds, Is.Not.EqualTo(changedSeed.EnemyDefinitionIds));
|
||||
Assert.That(first.EnemyDefinitions.Count, Is.EqualTo(first.ThreatCosts.Count));
|
||||
Assert.That(first.ComposedThreat, Is.GreaterThanOrEqualTo(first.ThreatBudget));
|
||||
Assert.That(first.ComposedThreat - first.ThreatBudget,
|
||||
Is.LessThanOrEqualTo(first.SelectedEntries.Max(entry => entry.ThreatCost) + 0.0001f));
|
||||
Assert.That(first.SelectedEntries.All(entry => SpeciesByTier[0].Contains(entry.EnemyDefinitionId)), Is.True);
|
||||
|
||||
Defense25DWaveComposition tier2 = pool.BuildWaveComposition(9, 180f, 11f, 417);
|
||||
Defense25DWaveComposition tier3 = pool.BuildWaveComposition(10, 360f, 25f, 417);
|
||||
Assert.That(tier2.TierId, Is.EqualTo(1));
|
||||
Assert.That(tier3.TierId, Is.EqualTo(2));
|
||||
Assert.That(tier2.SelectedEntries.All(entry => SpeciesByTier[1].Contains(entry.EnemyDefinitionId)), Is.True);
|
||||
Assert.That(tier3.SelectedEntries.All(entry => SpeciesByTier[2].Contains(entry.EnemyDefinitionId)), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DiversityConstraintRelaxesAndRecordsWhenNoCandidateFits()
|
||||
{
|
||||
Defense25DMonsterPoolDefinition pool = MakePool();
|
||||
Defense25DWaveComposition composition = pool.BuildWaveComposition(0, 0f, 1f, 1);
|
||||
|
||||
Assert.That(composition.DiversityConstraintRelaxed, Is.True);
|
||||
Assert.That(composition.SelectedEntries, Is.Not.Empty);
|
||||
Assert.That(composition.ComposedThreat, Is.GreaterThanOrEqualTo(1f));
|
||||
Assert.That(composition.ComposedThreat - 1f,
|
||||
Is.LessThanOrEqualTo(composition.SelectedEntries.Max(entry => entry.ThreatCost) + 0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompositionUsesEntryWeightsForReplacementDraws()
|
||||
{
|
||||
Defense25DMonsterPoolDefinition pool = MakePool();
|
||||
Set(pool.Tiers[0].Entries[0], "weight", 100f);
|
||||
int slimeSelections = 0;
|
||||
for (int i = 0; i < 64; i++)
|
||||
{
|
||||
Defense25DWaveComposition composition = pool.BuildWaveComposition(i, 0f, 1f, i + 1000);
|
||||
if (composition.EnemyDefinitionIds[0] == "Slime") slimeSelections++;
|
||||
}
|
||||
Assert.That(slimeSelections, Is.GreaterThan(45));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InvalidCatalogAndArgumentsAreReportedAsConfigurationErrors()
|
||||
{
|
||||
Defense25DMonsterPoolDefinition pool = MakePool();
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => pool.BuildWaveComposition(-1, 0f, 1f, 1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => pool.BuildWaveComposition(0, 0f, 0f, 1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => pool.BuildWaveComposition(0, float.NaN, 1f, 1));
|
||||
|
||||
Defense25DMonsterPoolEntry entry = pool.Tiers[0].Entries[0];
|
||||
Set(entry, "threatCost", 0f);
|
||||
Assert.That(pool.TryValidate(out string error), Is.False);
|
||||
Assert.That(error, Does.Contain("finite positive Weight and ThreatCost"));
|
||||
Assert.Throws<InvalidOperationException>(() => pool.BuildWaveComposition(0, 0f, 1f, 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CatalogRejectsExcludedHumanAndBadSourceReference()
|
||||
{
|
||||
Defense25DMonsterPoolDefinition pool = MakePool();
|
||||
Defense25DMonsterPoolEntry entry = pool.Tiers[0].Entries[0];
|
||||
Set(entry, "sourceCharacterId", "Archer");
|
||||
Assert.That(pool.TryValidate(out string error), Is.False);
|
||||
Assert.That(error, Does.Contain("Excluded"));
|
||||
|
||||
Set(entry, "sourceCharacterId", "Orc");
|
||||
Assert.That(pool.TryValidate(out error), Is.False);
|
||||
Assert.That(error, Does.Contain("mismatched SourceCharacterId"));
|
||||
|
||||
Set(entry, "sourceCharacterId", "Slime");
|
||||
Set(entry, "enemyDefinitionId", "Warlock");
|
||||
Assert.That(pool.TryValidate(out error), Is.False);
|
||||
Assert.That(error, Does.Contain("Excluded"));
|
||||
}
|
||||
|
||||
private void AssertTier(float nominalStart, int expected)
|
||||
{
|
||||
Assert.That(Defense25DMonsterPoolDefinition.TryGetTierForNominalStart(nominalStart, out int tier), Is.True);
|
||||
Assert.That(tier, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
private void AssertInvalidTier(float nominalStart)
|
||||
{
|
||||
Assert.That(Defense25DMonsterPoolDefinition.TryGetTierForNominalStart(nominalStart, out int tier), Is.False);
|
||||
Assert.That(tier, Is.EqualTo(-1));
|
||||
}
|
||||
|
||||
private Defense25DMonsterPoolDefinition MakePool()
|
||||
{
|
||||
Defense25DMonsterPoolDefinition pool = ScriptableObject.CreateInstance<Defense25DMonsterPoolDefinition>();
|
||||
created.Add(pool);
|
||||
float[] starts = { 0f, 180f, 360f };
|
||||
float[] costs = { 1.1f, 1.8f, 3f };
|
||||
Defense25DMonsterTierPool[] tiers = new Defense25DMonsterTierPool[3];
|
||||
for (int tierIndex = 0; tierIndex < tiers.Length; tierIndex++)
|
||||
{
|
||||
Defense25DMonsterTierPool tier = new Defense25DMonsterTierPool();
|
||||
Set(tier, "tierId", tierIndex);
|
||||
Set(tier, "fromCombatSeconds", starts[tierIndex]);
|
||||
Defense25DMonsterPoolEntry[] entries = new Defense25DMonsterPoolEntry[SpeciesByTier[tierIndex].Length];
|
||||
for (int itemIndex = 0; itemIndex < entries.Length; itemIndex++)
|
||||
{
|
||||
string id = SpeciesByTier[tierIndex][itemIndex];
|
||||
Defense25DMonsterAttackArchetype archetype = GetArchetype(id);
|
||||
Defense25DMonsterCombatProfile profile = new Defense25DMonsterCombatProfile();
|
||||
Set(profile, "definitionBasisId", GetBasis(tierIndex, itemIndex));
|
||||
Set(profile, "attackAction", "Attack");
|
||||
Set(profile, "archetype", archetype);
|
||||
Set(profile, "attackShape", archetype == Defense25DMonsterAttackArchetype.Contact
|
||||
? EnemyAttackShape.Body : archetype == Defense25DMonsterAttackArchetype.Melee
|
||||
? EnemyAttackShape.Box : archetype == Defense25DMonsterAttackArchetype.Charge
|
||||
? EnemyAttackShape.Cone : EnemyAttackShape.TargetCircle);
|
||||
Set(profile, "bodyRadius", 0.4f + itemIndex * 0.01f);
|
||||
Set(profile, "moveSpeed", 1.2f + itemIndex * 0.04f);
|
||||
Set(profile, "attackDamage", 6f + itemIndex);
|
||||
Set(profile, "warningSeconds", 0.5f);
|
||||
Set(profile, "activeSeconds", 0.18f);
|
||||
Set(profile, "recoverySeconds", 0.65f);
|
||||
Set(profile, "attackRange", 0.8f + itemIndex * 0.05f);
|
||||
Set(profile, "attackLength", archetype == Defense25DMonsterAttackArchetype.Charge ? 1.2f : 0.8f);
|
||||
Set(profile, "attackWidth", 0.6f);
|
||||
Set(profile, "attackRadius", archetype == Defense25DMonsterAttackArchetype.Area ? 1.1f : 0.5f);
|
||||
Set(profile, "projectileSpeed", archetype == Defense25DMonsterAttackArchetype.Projectile ? 4.5f : 0f);
|
||||
Set(profile, "projectileRadius", archetype == Defense25DMonsterAttackArchetype.Projectile ? 0.12f : 0f);
|
||||
|
||||
EnemyDefinition definition = ScriptableObject.CreateInstance<EnemyDefinition>();
|
||||
created.Add(definition);
|
||||
Defense25DMonsterPoolEntry entry = new Defense25DMonsterPoolEntry();
|
||||
Set(entry, "definition", definition);
|
||||
GameObject visualPrefab = new GameObject(id + " Visual Fixture");
|
||||
created.Add(visualPrefab);
|
||||
Set(entry, "visualPrefab", visualPrefab);
|
||||
Set(entry, "enemyDefinitionId", id);
|
||||
Set(entry, "sourceCharacterId", GetSourceId(id));
|
||||
Set(entry, "weight", 1f + itemIndex % 3 * 0.25f);
|
||||
Set(entry, "threatCost", costs[tierIndex]);
|
||||
Set(entry, "combatProfile", profile);
|
||||
entries[itemIndex] = entry;
|
||||
}
|
||||
Set(tier, "entries", entries);
|
||||
tiers[tierIndex] = tier;
|
||||
}
|
||||
Set(pool, "tiers", tiers);
|
||||
return pool;
|
||||
}
|
||||
|
||||
private static Defense25DMonsterAttackArchetype GetArchetype(string id)
|
||||
{
|
||||
return id switch
|
||||
{
|
||||
"Slime" => Defense25DMonsterAttackArchetype.Contact,
|
||||
"Bat" or "Hellbat" or "Hellhound" or "ArmoredOrc" or "OrcRider" or "Demon_B" or "Minotaur" => Defense25DMonsterAttackArchetype.Charge,
|
||||
"LavaSlime" or "Werebear" or "BloodMonster_B" or "Ghostfire" or "Demon_C" or "Demoness_B" or "FlameGolem" => Defense25DMonsterAttackArchetype.Area,
|
||||
"SkeletonArcher" or "BloodMonster_A" or "EyeballMonster" or "Demon_A" or "Demon_E" or "Demoness_A" => Defense25DMonsterAttackArchetype.Projectile,
|
||||
_ => Defense25DMonsterAttackArchetype.Melee,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetBasis(int tier, int index)
|
||||
{
|
||||
if (tier == 0) return new[] { "Slime", "Bat", "Skeleton", "Skeleton", "Bat", "Werewolf", "Slime" }[index];
|
||||
if (tier == 1) return new[] { "ArmoredSkeleton", "SkeletonArcher", "Werewolf", "Werebear", "ArmoredSkeleton", "SkeletonArcher", "Werewolf", "SkeletonArcher", "Bat" }[index];
|
||||
return new[] { "GreatswordSkeleton", "Werebear", "Werewolf", "SkeletonArcher", "GreatswordSkeleton", "Slime", "Werewolf", "SkeletonArcher", "SkeletonArcher", "Bat", "Werebear", "Werebear" }[index];
|
||||
}
|
||||
|
||||
private static string GetSourceId(string id)
|
||||
{
|
||||
return id switch
|
||||
{
|
||||
"LavaSlime" => "Lava Slime", "ArmoredSkeleton" => "Armored Skeleton", "SkeletonArcher" => "Skeleton Archer",
|
||||
"ArmoredOrc" => "Armored Orc", "BloodMonster_A" => "Blood Monster_A", "BloodMonster_B" => "Blood Monster_B",
|
||||
"EyeballMonster" => "Eyeball Monster", "GreatswordSkeleton" => "Greatsword Skeleton", "EliteOrc" => "Elite Orc",
|
||||
"OrcRider" => "Orc rider", "FlameGolem" => "Flame Golem", _ => id,
|
||||
};
|
||||
}
|
||||
|
||||
private static void Set(object instance, string name, object value)
|
||||
{
|
||||
FieldInfo field = instance.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null, $"Missing field {instance.GetType().Name}.{name}");
|
||||
field.SetValue(instance, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user