352 lines
15 KiB
C#
352 lines
15 KiB
C#
using System.Collections.Generic;
|
|
using BumpCombat.Combat;
|
|
using BumpCombat.Constants;
|
|
using BumpCombat.Defense.Comparison;
|
|
using BumpCombat.Enemies;
|
|
using BumpCombat.Player;
|
|
using BumpCombat.Progression;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace BumpCombat.Tests
|
|
{
|
|
public sealed class Defense25DRelicRuntimeTests
|
|
{
|
|
private PassiveRelicConstants tuning;
|
|
private Defense25DLoadout loadout;
|
|
private readonly List<ActiveArtifactDefinition> artifactDefinitions = new();
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
tuning = ScriptableObject.CreateInstance<PassiveRelicConstants>();
|
|
loadout = new Defense25DLoadout(0f, 100f, 6, 6);
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
Object.DestroyImmediate(tuning);
|
|
foreach (ActiveArtifactDefinition definition in artifactDefinitions)
|
|
Object.DestroyImmediate(definition);
|
|
artifactDefinitions.Clear();
|
|
}
|
|
|
|
[Test]
|
|
public void TwinHornOnlyDoublesOrdinaryUncoloredBumpsWhenTheConfiguredRollSucceeds()
|
|
{
|
|
tuning.TwinHornDoubleDamageChance = 1f;
|
|
loadout.TryAcquireRelic(PassiveRelicId.TwinHornTalisman);
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects());
|
|
|
|
Assert.That(runtime.ApplyOrdinaryBumpDamage(10f, true, false), Is.EqualTo(20f));
|
|
Assert.That(runtime.ApplyOrdinaryBumpDamage(10f, false, false), Is.EqualTo(10f));
|
|
Assert.That(runtime.ApplyOrdinaryBumpDamage(10f, true, true), Is.EqualTo(10f));
|
|
}
|
|
|
|
[Test]
|
|
public void WaveNailAppliesSourceDamageToNearbyEnemiesAndStartsCooldownOnlyOnAppliedHit()
|
|
{
|
|
tuning.WaveNailChance = 1f;
|
|
tuning.WaveNailDamageFraction = .6f;
|
|
tuning.WaveNailCooldownSeconds = 1.5f;
|
|
loadout.TryAcquireRelic(PassiveRelicId.WaveNail);
|
|
int applied = 0;
|
|
int attempts = 0;
|
|
bool allowDamage = false;
|
|
float observedDamage = 0f;
|
|
EnemyDamageSource observedSource = EnemyDamageSource.None;
|
|
Defense25DRelicTarget nearby = Target(2, new Vector3(1f, 0f, 0f));
|
|
Defense25DRelicEffects effects = new()
|
|
{
|
|
QueryEnemies = (_, _) => new[] { nearby },
|
|
DamageEnemy = (_, damage, _, source) =>
|
|
{
|
|
attempts++;
|
|
observedDamage = damage;
|
|
observedSource = source;
|
|
if (!allowDamage) return false;
|
|
applied++;
|
|
return true;
|
|
},
|
|
};
|
|
Defense25DRelicRuntime runtime = NewRuntime(effects);
|
|
runtime.BodyDiameterProvider = () => 1f;
|
|
runtime.NotifyHit(new Defense25DRelicHit(Target(1, Vector3.zero), Vector3.zero,
|
|
Vector3.right, 10f, 10f, true), 100f);
|
|
allowDamage = true;
|
|
runtime.NotifyHit(new Defense25DRelicHit(Target(0, Vector3.zero), Vector3.zero,
|
|
Vector3.right, 10f, 10f, true), 100f);
|
|
runtime.Advance(1f, 100f);
|
|
runtime.NotifyHit(new Defense25DRelicHit(Target(0, Vector3.zero), Vector3.zero,
|
|
Vector3.right, 10f, 10f, true), 100f);
|
|
|
|
Assert.That(applied, Is.EqualTo(1));
|
|
Assert.That(attempts, Is.EqualTo(2));
|
|
Assert.That(observedDamage, Is.EqualTo(6f));
|
|
Assert.That(observedSource, Is.EqualTo(EnemyDamageSource.PassiveRelicAttack));
|
|
}
|
|
|
|
[Test]
|
|
public void CounterBellSlowsEveryLivingEnemyInItsQueryInsteadOfStoppingAtFirstSuccess()
|
|
{
|
|
tuning.CounterBellChance = 1f;
|
|
loadout.TryAcquireRelic(PassiveRelicId.CounterBell);
|
|
List<int> slowed = new();
|
|
Defense25DRelicTarget[] targets =
|
|
{
|
|
Target(1, Vector3.zero),
|
|
Target(2, Vector3.right),
|
|
new(3, Vector3.forward, 10f, 10f, EnemyDamageSource.None, true),
|
|
};
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects
|
|
{
|
|
QueryEnemies = (_, _) => targets,
|
|
ApplySlow = (target, _, _) => { slowed.Add(target.Identity); return true; },
|
|
});
|
|
runtime.BodyDiameterProvider = () => 1f;
|
|
|
|
runtime.NotifyPlayerDamageTaken(1f, 99f, 100f, Vector3.zero);
|
|
|
|
Assert.That(slowed, Is.EquivalentTo(new[] { 1, 2 }));
|
|
}
|
|
|
|
[Test]
|
|
public void LethalValidHitStillRunsWaveNailBloodFangAndResonanceCore()
|
|
{
|
|
tuning.WaveNailChance = 1f;
|
|
tuning.ResonanceEchoChance = 1f;
|
|
tuning.BloodFangChance = 1f;
|
|
loadout.TryAcquireRelic(PassiveRelicId.WaveNail);
|
|
loadout.TryAcquireRelic(PassiveRelicId.BloodFang);
|
|
loadout.TryAcquireRelic(PassiveRelicId.ResonanceCore);
|
|
ActiveArtifactDefinition artifact = Artifact(ActiveArtifactEffect.Pulse);
|
|
Assert.That(loadout.TryAddArtifact(artifact), Is.True);
|
|
int damageCalls = 0, healCalls = 0, echoCalls = 0;
|
|
Defense25DRelicTarget nearby = Target(11, Vector3.right);
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects
|
|
{
|
|
QueryEnemies = (_, _) => new[] { nearby },
|
|
DamageEnemy = (_, _, _, _) => { damageCalls++; return true; },
|
|
HealPlayer = _ => { healCalls++; return true; },
|
|
EmitEcho = (use, _, _, _) =>
|
|
{
|
|
Assert.That(use.Definition, Is.SameAs(artifact));
|
|
echoCalls++;
|
|
return true;
|
|
},
|
|
});
|
|
runtime.BodyDiameterProvider = () => 1f;
|
|
runtime.NotifyHit(new Defense25DRelicHit(
|
|
Target(10, Vector3.zero, isDead: true), Vector3.zero, Vector3.right,
|
|
10f, 10f, true), 50f, 100f);
|
|
|
|
Assert.That(damageCalls, Is.EqualTo(1));
|
|
Assert.That(healCalls, Is.EqualTo(1));
|
|
Assert.That(echoCalls, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void WaveNailCanDamageTheOriginalHitTargetWhenItSurvivesTheBump()
|
|
{
|
|
tuning.WaveNailChance = 1f;
|
|
loadout.TryAcquireRelic(PassiveRelicId.WaveNail);
|
|
List<int> damaged = new();
|
|
Defense25DRelicTarget struck = Target(0, Vector3.zero);
|
|
Defense25DRelicTarget neighbor = Target(1, Vector3.right);
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects
|
|
{
|
|
QueryEnemies = (_, _) => new[] { struck, neighbor },
|
|
DamageEnemy = (target, _, _, _) => { damaged.Add(target.Identity); return true; },
|
|
});
|
|
runtime.BodyDiameterProvider = () => 1f;
|
|
|
|
runtime.NotifyHit(new Defense25DRelicHit(struck, Vector3.zero, Vector3.right,
|
|
10f, 10f, true), 100f);
|
|
|
|
Assert.That(damaged, Is.EquivalentTo(new[] { 0, 1 }));
|
|
}
|
|
|
|
[Test]
|
|
public void WaveNailIncludesAnEnemyWhoseBodyOverlapsTheAreaAtItsOuterEdge()
|
|
{
|
|
tuning.WaveNailChance = 1f;
|
|
loadout.TryAcquireRelic(PassiveRelicId.WaveNail);
|
|
List<int> damaged = new();
|
|
Defense25DRelicTarget boundaryTarget = Target(12, new Vector3(3.2f, 0f, 0f));
|
|
const float targetBodyRadius = .3f;
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects
|
|
{
|
|
QueryEnemies = (center, areaRadius) =>
|
|
Vector3.Distance(new Vector3(center.x, 0f, center.z),
|
|
new Vector3(boundaryTarget.Position.x, 0f, boundaryTarget.Position.z))
|
|
<= areaRadius + targetBodyRadius
|
|
? new[] { boundaryTarget }
|
|
: System.Array.Empty<Defense25DRelicTarget>(),
|
|
DamageEnemy = (target, _, _, _) => { damaged.Add(target.Identity); return true; },
|
|
});
|
|
runtime.BodyDiameterProvider = () => 1f;
|
|
|
|
runtime.NotifyHit(new Defense25DRelicHit(Target(0, Vector3.zero), Vector3.zero,
|
|
Vector3.right, 10f, 10f, true), 100f);
|
|
|
|
Assert.That(damaged, Is.EqualTo(new[] { boundaryTarget.Identity }));
|
|
}
|
|
|
|
[Test]
|
|
public void RiftWedgeAppliesThunderCrashVulnerabilityOnTheConfiguredOrdinaryHitCount()
|
|
{
|
|
tuning.RiftWedgeRequiredHits = 3;
|
|
loadout.TryAcquireRelic(PassiveRelicId.RiftWedge);
|
|
ActiveArtifactDefinition thunder = Artifact(ActiveArtifactEffect.ThunderCrash,
|
|
normalVulnerabilityDuration: 2.5f, normalVulnerabilityIncrease: .3f);
|
|
loadout.ConfigureCatalog(new[] { thunder });
|
|
int applied = 0;
|
|
Defense25DRelicEffects effects = new()
|
|
{
|
|
ApplyVulnerability = (_, duration, increase) =>
|
|
{
|
|
Assert.That(duration, Is.EqualTo(2.5f));
|
|
Assert.That(increase, Is.EqualTo(.3f));
|
|
applied++;
|
|
return true;
|
|
},
|
|
};
|
|
Defense25DRelicRuntime runtime = NewRuntime(effects);
|
|
Defense25DRelicTarget target = Target(7, Vector3.zero);
|
|
|
|
for (int i = 0; i < 2; i++)
|
|
runtime.NotifyHit(new Defense25DRelicHit(target, Vector3.zero, Vector3.right,
|
|
1f, 0f, true), 100f);
|
|
Assert.That(applied, Is.Zero);
|
|
runtime.NotifyHit(new Defense25DRelicHit(target, Vector3.zero, Vector3.right,
|
|
1f, 0f, true), 100f);
|
|
|
|
Assert.That(applied, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void BondLedgerPaysOnlyForTowerKillsAfterRecentBumpAndDeduplicatesDeaths()
|
|
{
|
|
tuning.BondLedgerChance = 1f;
|
|
tuning.BondLedgerCurrency = 2;
|
|
tuning.BondLedgerBumpWindowSeconds = 3f;
|
|
loadout.TryAcquireRelic(PassiveRelicId.BondLedger);
|
|
int currency = 0;
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects
|
|
{
|
|
AwardCurrency = amount => { currency += amount; return true; },
|
|
});
|
|
Defense25DRelicTarget target = Target(0, Vector3.zero);
|
|
runtime.NotifyHit(new Defense25DRelicHit(target, Vector3.zero, Vector3.right,
|
|
1f, 0f, true), 100f);
|
|
runtime.Advance(1f, 100f);
|
|
Defense25DRelicTarget towerKill = Target(0, Vector3.zero, EnemyDamageSource.Tower);
|
|
|
|
runtime.NotifyEnemyDeath(towerKill, 100f);
|
|
runtime.NotifyEnemyDeath(towerKill, 100f);
|
|
|
|
Assert.That(currency, Is.EqualTo(2));
|
|
}
|
|
|
|
[Test]
|
|
public void RunResetClearsRuntimeTimersAndTemporaryModifiersThroughOwnerCallbacks()
|
|
{
|
|
tuning.HunterFeatherChance = 1f;
|
|
loadout.TryAcquireRelic(PassiveRelicId.HunterFeather);
|
|
int added = 0, removed = 0;
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects
|
|
{
|
|
SetTimedPlayerModifier = (_, _) => added++,
|
|
RemovePlayerModifier = _ => removed++,
|
|
});
|
|
runtime.NotifyEnemyDeath(Target(8, Vector3.zero, EnemyDamageSource.Player), 100f);
|
|
Assert.That(runtime.HunterExpiresAt, Is.GreaterThan(0f));
|
|
|
|
runtime.ResetRun(4f);
|
|
|
|
Assert.That(runtime.IsActive, Is.False);
|
|
Assert.That(runtime.HunterExpiresAt, Is.Zero);
|
|
Assert.That(added, Is.EqualTo(1));
|
|
Assert.That(removed, Is.GreaterThanOrEqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void ResonantCrystalRepeatExpiresIfItsDelayCompletesWhilePlayerIsDead()
|
|
{
|
|
tuning.ResonantCrystalChance = 1f;
|
|
tuning.ResonantCrystalDelaySeconds = 1f;
|
|
ActiveArtifactDefinition artifact = Artifact(ActiveArtifactEffect.Cyclone);
|
|
Assert.That(loadout.TryAddArtifact(artifact), Is.True);
|
|
Assert.That(loadout.TryAcquireRelic(PassiveRelicId.ResonantCrystal), Is.True);
|
|
int repeats = 0;
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects
|
|
{
|
|
EmitRepeat = (_, _, _, _) => { repeats++; return true; },
|
|
});
|
|
|
|
runtime.NotifyArtifactUse(new Defense25DArtifactUse(artifact, false),
|
|
Vector3.zero, Vector3.right, 100f);
|
|
Assert.That(runtime.IsRepeatPending, Is.True);
|
|
|
|
runtime.Advance(1f, 0f);
|
|
runtime.Advance(2f, 100f);
|
|
|
|
Assert.That(runtime.IsRepeatPending, Is.False);
|
|
Assert.That(repeats, Is.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void ResonantCrystalRepeatStillFiresAfterDeathIfDelayHasNotEndedYet()
|
|
{
|
|
tuning.ResonantCrystalChance = 1f;
|
|
tuning.ResonantCrystalDelaySeconds = 1f;
|
|
ActiveArtifactDefinition artifact = Artifact(ActiveArtifactEffect.Cyclone);
|
|
Assert.That(loadout.TryAddArtifact(artifact), Is.True);
|
|
Assert.That(loadout.TryAcquireRelic(PassiveRelicId.ResonantCrystal), Is.True);
|
|
int repeats = 0;
|
|
Defense25DRelicRuntime runtime = NewRuntime(new Defense25DRelicEffects
|
|
{
|
|
EmitRepeat = (_, _, _, _) => { repeats++; return true; },
|
|
});
|
|
|
|
runtime.NotifyArtifactUse(new Defense25DArtifactUse(artifact, false),
|
|
Vector3.zero, Vector3.right, 100f);
|
|
runtime.Advance(.5f, 0f);
|
|
Assert.That(runtime.IsRepeatPending, Is.True);
|
|
|
|
runtime.Advance(1f, 100f);
|
|
|
|
Assert.That(runtime.IsRepeatPending, Is.False);
|
|
Assert.That(repeats, Is.EqualTo(1));
|
|
}
|
|
|
|
private Defense25DRelicRuntime NewRuntime(Defense25DRelicEffects effects)
|
|
{
|
|
Defense25DRelicRuntime runtime = new(loadout, tuning, effects, () => 0f);
|
|
runtime.BeginRun();
|
|
return runtime;
|
|
}
|
|
|
|
private ActiveArtifactDefinition Artifact(
|
|
ActiveArtifactEffect effect, float normalVulnerabilityDuration = 0f,
|
|
float normalVulnerabilityIncrease = 0f)
|
|
{
|
|
ActiveArtifactDefinition definition = ScriptableObject.CreateInstance<ActiveArtifactDefinition>();
|
|
definition.Configure(effect.ToString(), effect.ToString(), "*", effect,
|
|
DamageTag.Collision, Color.white, Color.white, 0f, 0f, 1f,
|
|
1f, 1f, 1f, 1f, 1f, 1f,
|
|
normalBumpVulnerabilityIncrease: normalVulnerabilityIncrease,
|
|
normalBumpVulnerabilityDuration: normalVulnerabilityDuration);
|
|
artifactDefinitions.Add(definition);
|
|
return definition;
|
|
}
|
|
|
|
private static Defense25DRelicTarget Target(int id, Vector3 position,
|
|
EnemyDamageSource source = EnemyDamageSource.None, bool isDead = false) =>
|
|
new(id, position, 100f, 100f, source, isDead);
|
|
}
|
|
|
|
}
|