Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/Defense25DRelicRuntimeTests.cs
T
2026-09-27 21:08:05 +09:00

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