Implement passive relics and six-family defense towers

This commit is contained in:
2026-09-23 18:11:38 +09:00
parent 5d11288a6f
commit c94cbda1e6
21 changed files with 2882 additions and 152 deletions
@@ -151,6 +151,344 @@ namespace BumpCombat.PlayModeTests
}
}
[UnityTest]
public IEnumerator DefensePrototype_DefaultBuildsCanDamageTheirPortalLanes()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
MovePlayerNear(player, controller.Tuning.GetBuildPadPosition(0));
Assert.That(controller.TryBuildTowerAtPad(0), Is.True);
MovePlayerNear(player, controller.Tuning.GetBuildPadPosition(1));
Assert.That(controller.TryBuildTowerAtPad(1), Is.True);
DefenseTower upper = FindTower(0, DefenseTowerKind.Summon);
DefenseTower lower = FindTower(1, DefenseTowerKind.Direct);
Assert.That(upper, Is.Not.Null, "Upper portal is immune to fire.");
Assert.That(lower, Is.Not.Null, "Lower portal is immune to Summon.");
yield return WaitForRouteAgents(2);
DefenseRouteAgent upperAgent = FindAgentWithImmunity(DefenseTowerKind.Direct);
DefenseRouteAgent lowerAgent = FindAgentWithImmunity(DefenseTowerKind.Summon);
yield return WaitForAgentReady(upperAgent);
yield return WaitForAgentReady(lowerAgent);
FreezeEnemyControllers(FindRouteAgents());
float upperHealth = upperAgent.Enemy.CurrentHealth;
float lowerHealth = lowerAgent.Enemy.CurrentHealth;
Assert.That(upper.TryAttack(upperAgent), Is.True);
Assert.That(lowerAgent.Enemy.TryReposition(lower.transform.position), Is.True);
Assert.That(lower.TryPulse(), Is.True);
Assert.That(upperAgent.Enemy.CurrentHealth, Is.LessThan(upperHealth));
Assert.That(lowerAgent.Enemy.CurrentHealth, Is.LessThan(lowerHealth));
}
[UnityTest]
public IEnumerator DefensePrototype_AllSixTowersShowTheirGameplayRanges()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(200), Is.True);
DefenseTowerKind[] kinds =
{
DefenseTowerKind.Direct, DefenseTowerKind.Summon,
DefenseTowerKind.Lightning, DefenseTowerKind.Cold,
DefenseTowerKind.Seismic, DefenseTowerKind.Empowering,
};
for (int pad = 0; pad < kinds.Length; pad++)
{
DefenseTower tower = BuildTower(controller, player, pad, kinds[pad]);
Transform range = tower.transform.Find("Effect Range");
Assert.That(range, Is.Not.Null, $"{kinds[pad]} range is missing.");
LineRenderer line = range.GetComponent<LineRenderer>();
Assert.That(line, Is.Not.Null);
Assert.That(line.positionCount, Is.GreaterThan(16));
float expected = kinds[pad] == DefenseTowerKind.Summon
? controller.Tuning.SummonAreaRadius
: kinds[pad] == DefenseTowerKind.Empowering
? controller.Tuning.SupportRadius : controller.Tuning.TowerRange;
Assert.That(line.GetPosition(0).x * tower.transform.lossyScale.x,
Is.EqualTo(expected).Within(0.02f));
}
DefenseTower fire = FindTower(0, DefenseTowerKind.Direct);
MovePlayerNear(player, fire.transform.position);
Assert.That(controller.TryUpgradeTowerAtPad(0, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(0, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(0, DefenseTowerBranch.Second), Is.True);
LineRenderer fireRange = fire.transform.Find("Effect Range")
.GetComponent<LineRenderer>();
Assert.That(fireRange.GetPosition(0).x * fire.transform.lossyScale.x,
Is.EqualTo(controller.Tuning.TowerRange * 0.5f).Within(0.02f));
}
[UnityTest]
public IEnumerator DefensePrototype_DamageTowersShowSuccessfulAttackEffects()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(200), Is.True);
yield return WaitForRouteAgents(2);
DefenseRouteAgent target = FindAgentWithImmunity(DefenseTowerKind.Summon);
yield return WaitForAgentReady(target);
FreezeEnemyControllers(FindRouteAgents());
DefenseTowerKind[] kinds =
{
DefenseTowerKind.Direct, DefenseTowerKind.Lightning,
DefenseTowerKind.Cold, DefenseTowerKind.Seismic,
};
for (int i = 0; i < kinds.Length; i++)
{
DefenseTower tower = BuildTower(controller, player, i, kinds[i]);
Assert.That(target.Enemy.TryReposition(tower.transform.position), Is.True);
Physics2D.SyncTransforms();
Assert.That(tower.TryPulse(), Is.True, $"{kinds[i]} should attack.");
GameObject effect = GameObject.Find($"{kinds[i]} Attack Effect");
Assert.That(effect, Is.Not.Null, $"{kinds[i]} attack effect is missing.");
SpriteRenderer impact = effect.transform.Find("Impact Sprite")
?.GetComponent<SpriteRenderer>();
Assert.That(impact, Is.Not.Null);
Assert.That(impact.sprite, Is.Not.Null,
"The placeholder impact must be replaceable by an imported sprite.");
}
}
[UnityTest]
public IEnumerator DefensePrototype_TowerAutomaticallyAttacksTargetInRange()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
DefenseTower fire = BuildTower(controller, player, 1, DefenseTowerKind.Direct);
yield return WaitForRouteAgents(2);
DefenseRouteAgent target = FindAgentWithImmunity(DefenseTowerKind.Summon);
yield return WaitForAgentReady(target);
FreezeEnemyControllers(FindRouteAgents());
Assert.That(target.Enemy.TryReposition(
(Vector2)fire.transform.position + Vector2.left), Is.True);
Physics2D.SyncTransforms();
float health = target.Enemy.CurrentHealth;
bool effectSeen = false;
float deadline = Time.realtimeSinceStartup + 2f;
while (Time.realtimeSinceStartup < deadline
&& (target.Enemy.CurrentHealth >= health || !effectSeen))
{
yield return null;
effectSeen |= GameObject.Find("Direct Attack Effect") != null;
}
Assert.That(target.Enemy.CurrentHealth, Is.LessThan(health),
"Tower Update must acquire and damage a nonimmune route enemy.");
Assert.That(effectSeen, Is.True,
"An automatic attack must show the same effect as a manual pulse.");
}
[UnityTest]
public IEnumerator DefensePrototype_SummonPlaysExistingAttackSpriteOnHit()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
DefenseTower summon = BuildTower(
controller, player, 0, DefenseTowerKind.Summon);
yield return WaitForRouteAgents(2);
DefenseRouteAgent target = FindAgentWithImmunity(DefenseTowerKind.Direct);
yield return WaitForAgentReady(target);
FreezeEnemyControllers(FindRouteAgents());
DefenseSummonUnit unit = summon.GetComponentInChildren<DefenseSummonUnit>();
Assert.That(unit, Is.Not.Null);
Assert.That(target.Enemy.TryReposition(unit.transform.position), Is.True);
Physics2D.SyncTransforms();
bool attackSpriteSeen = false;
bool effectSeen = false;
float deadline = Time.realtimeSinceStartup + 2f;
while (Time.realtimeSinceStartup < deadline
&& (!attackSpriteSeen || !effectSeen))
{
yield return null;
Sprite sprite = unit.GetComponent<SpriteRenderer>().sprite;
attackSpriteSeen |= sprite != null
&& sprite.name.Contains("BumpAttack");
effectSeen |= GameObject.Find("Summon Attack Effect") != null;
}
Assert.That(attackSpriteSeen, Is.True,
"The summoned swordsman must play the existing attack sheet.");
Assert.That(effectSeen, Is.True,
"A successful summon hit must show its impact.");
}
[UnityTest]
public IEnumerator DefensePrototype_AllSixFamiliesBuildAndUpgradeToDistinctFinalBranches()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(1000), Is.True);
DefenseTowerKind[] kinds =
{
DefenseTowerKind.Direct,
DefenseTowerKind.Summon,
DefenseTowerKind.Lightning,
DefenseTowerKind.Cold,
DefenseTowerKind.Seismic,
DefenseTowerKind.Empowering,
};
for (int pad = 0; pad < kinds.Length; pad++)
{
DefenseTower tower = BuildTower(controller, player, pad, kinds[pad]);
Assert.That(tower.Tier, Is.EqualTo(1));
Assert.That(controller.TryUpgradeTowerAtPad(
pad, DefenseTowerBranch.First), Is.False);
int beforeUpgrade = controller.ConstructionCurrency;
Assert.That(controller.TryUpgradeTowerAtPad(
pad, DefenseTowerBranch.None), Is.True);
Assert.That(tower.Tier, Is.EqualTo(2));
Assert.That(controller.ConstructionCurrency,
Is.EqualTo(beforeUpgrade - controller.Tuning.Tier2UpgradeCost));
Assert.That(controller.TryUpgradeTowerAtPad(
pad, DefenseTowerBranch.None), Is.True);
Assert.That(tower.Tier, Is.EqualTo(3));
Assert.That(controller.TryUpgradeTowerAtPad(
pad, DefenseTowerBranch.None), Is.False);
DefenseTowerBranch branch = pad % 2 == 0
? DefenseTowerBranch.First : DefenseTowerBranch.Second;
Assert.That(controller.TryUpgradeTowerAtPad(pad, branch), Is.True);
Assert.That(tower.Tier, Is.EqualTo(4));
Assert.That(tower.Branch, Is.EqualTo(branch));
Assert.That(DefenseTower.GetBranchName(kinds[pad], branch),
Is.Not.Empty);
Assert.That(controller.TryUpgradeTowerAtPad(
pad, DefenseTowerBranch.First), Is.False);
tower.enabled = false;
}
Assert.That(FindTowers().Length, Is.EqualTo(6));
}
[UnityTest]
public IEnumerator DefensePrototype_SentinelAndScoutHaveDifferentUnitCounts()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(500), Is.True);
DefenseTower sentinel = BuildTower(controller, player, 0,
DefenseTowerKind.Summon);
DefenseTower scout = BuildTower(controller, player, 1,
DefenseTowerKind.Summon);
Assert.That(player.TryReposition(sentinel.transform.position), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(0, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(0, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(0, DefenseTowerBranch.First), Is.True);
Assert.That(player.TryReposition(scout.transform.position), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(1, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(1, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(1, DefenseTowerBranch.Second), Is.True);
yield return null;
Assert.That(sentinel.GetComponentsInChildren<DefenseSummonUnit>().Length,
Is.EqualTo(1));
Assert.That(scout.GetComponentsInChildren<DefenseSummonUnit>().Length,
Is.EqualTo(6));
}
[UnityTest]
public IEnumerator DefensePrototype_SmotheringBoostsTowerDamageInsideSupportAura()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(200), Is.True);
yield return WaitForRouteAgents(2);
DefenseRouteAgent target = FindAgentWithImmunity(DefenseTowerKind.Summon);
Assert.That(target, Is.Not.Null);
yield return WaitForAgentReady(target);
FreezeEnemyControllers(FindRouteAgents());
DefenseTower fire = BuildTower(controller, player, 0, DefenseTowerKind.Direct);
DefenseTower support = BuildTower(controller, player, 4,
DefenseTowerKind.Empowering);
Assert.That(controller.TryUpgradeTowerAtPad(4, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(4, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(4, DefenseTowerBranch.Second), Is.True);
Assert.That(target.Enemy.TryReposition(support.transform.position), Is.True);
float health = target.Enemy.CurrentHealth;
Assert.That(controller.TryApplyTowerDamage(fire, target, 10f), Is.True);
float expected = BumpCombat.Combat.DamageCalculator.FinalizeDamage(
10f * (1f + controller.Tuning.SupportDamageBonusPerTier * 4)
* (1f + controller.Tuning.SmotheringDamageBonus));
Assert.That(health - target.Enemy.CurrentHealth,
Is.EqualTo(expected).Within(0.01f));
}
[UnityTest]
public IEnumerator DefensePrototype_EnemyDeathAwardsConstructionCurrencyOnceArrivalDoesNot()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
DefenseTower fire = BuildTower(controller, player, 0, DefenseTowerKind.Direct);
fire.enabled = false;
MovePlayerNear(player, Vector2.zero);
yield return WaitForRouteAgents(2);
DefenseRouteAgent killTarget = FindAgentWithImmunity(DefenseTowerKind.Summon);
DefenseRouteAgent arrivalTarget = FindAgentWithImmunity(DefenseTowerKind.Direct);
Assert.That(killTarget, Is.Not.Null);
Assert.That(arrivalTarget, Is.Not.Null);
yield return WaitForAgentReady(killTarget);
yield return WaitForAgentReady(arrivalTarget);
FreezeEnemyControllers(FindRouteAgents());
int currency = controller.ConstructionCurrency;
Assert.That(controller.TryApplyTowerDamage(fire, killTarget, 10000f), Is.True);
Assert.That(killTarget.Enemy.IsDead, Is.True);
Assert.That(controller.ConstructionCurrency,
Is.EqualTo(currency + controller.Tuning.EnemyKillCurrency));
Assert.That(controller.TryApplyTowerDamage(fire, killTarget, 10000f), Is.False);
Assert.That(controller.ConstructionCurrency,
Is.EqualTo(currency + controller.Tuning.EnemyKillCurrency));
DriveAgentToArrival(arrivalTarget);
Assert.That(controller.ConstructionCurrency,
Is.EqualTo(currency + controller.Tuning.EnemyKillCurrency));
}
[UnityTest]
public IEnumerator DefensePrototype_ColdSeismicAndImbuingHaveCombatEffects()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(500), Is.True);
yield return WaitForRouteAgents(2);
DefenseRouteAgent target = FindAgentWithImmunity(DefenseTowerKind.Direct);
Assert.That(target, Is.Not.Null);
yield return WaitForAgentReady(target);
FreezeEnemyControllers(FindRouteAgents());
DefenseTower cold = BuildTower(controller, player, 2, DefenseTowerKind.Cold);
DefenseTower support = BuildTower(controller, player, 5,
DefenseTowerKind.Empowering);
Assert.That(target.Enemy.TryReposition(cold.transform.position), Is.True);
float health = target.Enemy.CurrentHealth;
Assert.That(cold.TryPulse(), Is.True);
Assert.That(target.Enemy.CurrentHealth, Is.LessThan(health));
Assert.That(target.Enemy.MovementSpeedMultiplier, Is.LessThan(1f));
Assert.That(controller.TryUpgradeTowerAtPad(5, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(5, DefenseTowerBranch.None), Is.True);
Assert.That(controller.TryUpgradeTowerAtPad(5, DefenseTowerBranch.First), Is.True);
Assert.That(controller.GetTowerAttackIntervalMultiplier(cold),
Is.EqualTo(controller.Tuning.ImbuingAttackIntervalMultiplier));
DefenseTower seismic = BuildTower(controller, player, 4,
DefenseTowerKind.Seismic);
Assert.That(target.Enemy.TryReposition(seismic.transform.position), Is.True);
Assert.That(seismic.TryPulse(), Is.True);
Assert.That(target.Enemy.IsStunned, Is.True);
}
[UnityTest]
public IEnumerator DefensePrototype_TowerDamageUsesSightEfficiencyAndPortalImmunity()
{
@@ -452,15 +790,14 @@ namespace BumpCombat.PlayModeTests
Assert.That(relicReward, Is.Not.Null);
Assert.That(relicReward.DebugIsSelectionVisible, Is.True,
"Collecting a cleared portal reward must open its passive relic choice.");
Assert.That(relicReward.DebugShownCount, Is.EqualTo(3),
"The first portal must offer all three phase-five relics.");
Assert.That(relicReward.DebugShownPassiveRelicCount, Is.EqualTo(3),
"The first portal must offer three eligible relics from the current catalog.");
Assert.That(relicReward.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(
player.GetComponent<PassiveRelicController>().Owns(
PassiveRelicId.TwinHornTalisman),
Is.True,
"Choosing the first portal option must acquire the shown passive relic.");
player.GetComponent<PassiveRelicController>().OwnedCount,
Is.EqualTo(1),
"Choosing the first portal option must acquire one passive relic.");
Assert.That(RunManager.Instance.IsSelectionOpen, Is.False);
Assert.That(RunManager.Instance.IsRunCleared, Is.False,
"Collecting one portal reward must not finish the two-portal run.");
@@ -473,10 +810,12 @@ namespace BumpCombat.PlayModeTests
+ controller.Tuning.PortalRewardCurrency * 2));
Assert.That(relicReward.DebugIsSelectionVisible, Is.True,
"The second cleared portal must offer another passive relic choice.");
Assert.That(relicReward.DebugShownCount, Is.EqualTo(2),
"A relic already chosen at the first portal must not appear again.");
Assert.That(relicReward.DebugShownPassiveRelicCount, Is.EqualTo(3),
"The expanded catalog still offers three candidates at the second portal.");
Assert.That(relicReward.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(player.GetComponent<PassiveRelicController>().OwnedCount,
Is.EqualTo(2));
Assert.That(RunManager.Instance.IsRunCleared, Is.True,
"The run clears only after both portal relic choices are resolved.");
}
@@ -1022,6 +1361,419 @@ namespace BumpCombat.PlayModeTests
"A Thunder Crash echo must not grant its manual invulnerability.");
}
[UnityTest]
public IEnumerator WaveNail_UsesBaseBumpDamageAndDamagesEachEnemyOnce()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.WaveNail), Is.True);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position
+ new Vector2(4f, 4f);
EnemyController original = CreateRelicTestEnemy(origin, ArtifactColor.Red);
EnemyController nearby = CreateRelicTestEnemy(origin + Vector2.right, ArtifactColor.Red);
yield return null;
original.enabled = false;
nearby.enabled = false;
GameObject duplicateCollider = new("Wave duplicate collider");
duplicateCollider.transform.SetParent(nearby.transform, false);
duplicateCollider.AddComponent<CircleCollider2D>().radius = 0.25f;
Physics2D.SyncTransforms();
float before = nearby.CurrentHealth;
CombatHitResult hit = new(
passiveRelicPlayer.gameObject,
original.gameObject,
false,
HitSide.Front,
20f,
Vector2.right,
0f,
origin,
baseBumpDamage: 10f);
CombatEvents.RaiseValidHit(hit);
float expected = DamageCalculator.FinalizeDamage(
10f * GameplayConstants.Current.PassiveRelics.WaveNailDamageFraction);
Assert.That(nearby.CurrentHealth, Is.EqualTo(before - expected).Within(0.01f));
CombatEvents.RaiseValidHit(hit);
Assert.That(nearby.CurrentHealth, Is.EqualTo(before - expected).Within(0.01f),
"One relic cooldown must cover the next valid hit.");
}
[UnityTest]
public IEnumerator CapacitorRing_ExcludesOriginalAndLimitsChainToThree()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.CapacitorRing), Is.True);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position
+ new Vector2(4f, 4f);
EnemyController original = CreateRelicTestEnemy(origin, ArtifactColor.Red);
EnemyController[] neighbors = new EnemyController[4];
for (int i = 0; i < neighbors.Length; i++)
{
neighbors[i] = CreateRelicTestEnemy(
origin + Vector2.right * (0.5f + i * 0.5f),
ArtifactColor.Red);
}
yield return null;
original.enabled = false;
foreach (EnemyController neighbor in neighbors) neighbor.enabled = false;
Physics2D.SyncTransforms();
float originalBefore = original.CurrentHealth;
float[] before = System.Array.ConvertAll(neighbors, enemy => enemy.CurrentHealth);
CombatEvents.RaiseValidHit(new CombatHitResult(
passiveRelicPlayer.gameObject,
original.gameObject,
false,
HitSide.Front,
10f,
Vector2.right,
0f,
origin,
baseBumpDamage: 10f));
Assert.That(original.CurrentHealth, Is.EqualTo(originalBefore));
float expected = DamageCalculator.FinalizeDamage(
10f * GameplayConstants.Current.PassiveRelics.CapacitorRingDamageFraction);
for (int i = 0; i < neighbors.Length; i++)
{
Assert.That(neighbors[i].CurrentHealth,
Is.EqualTo(before[i] - (i < 3 ? expected : 0f)).Within(0.01f));
}
}
[UnityTest]
public IEnumerator EmberWick_AppliesExistingPhoenixIgnite()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.EmberWick), Is.True);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position
+ new Vector2(4f, 4f);
EnemyController target = CreateRelicTestEnemy(origin, ArtifactColor.Red);
yield return null;
target.enabled = false;
CombatEvents.RaiseValidHit(new CombatHitResult(
passiveRelicPlayer.gameObject,
target.gameObject,
false,
HitSide.Front,
10f,
Vector2.right,
0f,
origin,
baseBumpDamage: 10f));
Assert.That(target.IsIgnited, Is.True);
ActiveArtifactDefinition phoenix = null;
for (int i = 0; i < passiveRelicArtifacts.CatalogCount; i++)
{
ActiveArtifactDefinition definition = passiveRelicArtifacts.GetCatalogArtifactAt(i);
if (definition != null && definition.Effect == ActiveArtifactEffect.Phoenix)
{
phoenix = definition;
break;
}
}
Assert.That(phoenix, Is.Not.Null);
Assert.That(target.IgniteRemaining,
Is.EqualTo(phoenix.NormalIgniteDuration).Within(0.01f));
}
[UnityTest]
public IEnumerator RuptureSeed_TriggersFromDirectRelicAttackKill()
{
yield return PreparePassiveRelicFixture(null);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f);
EnemyController victim = CreateRelicTestEnemy(origin, ArtifactColor.Red);
EnemyController neighbor = CreateRelicTestEnemy(origin + Vector2.right, ArtifactColor.Red);
yield return null;
victim.enabled = false;
neighbor.enabled = false;
float before = neighbor.CurrentHealth;
Assert.That(victim.TryTakeDamage(victim.CurrentHealth + 1f, Vector2.zero,
0f, false, false, EnemyDamageSource.PassiveRelicAttack), Is.True);
Assert.That(neighbor.CurrentHealth, Is.LessThan(before));
}
[UnityTest]
public IEnumerator EmberCarrier_TransfersRemainingIgniteOnCreditedKill()
{
yield return PreparePassiveRelicFixture(ActiveArtifactEffect.Phoenix);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.EmberCarrier), Is.True);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f);
EnemyController victim = CreateRelicTestEnemy(origin, ArtifactColor.Red);
EnemyController neighbor = CreateRelicTestEnemy(origin + Vector2.right, ArtifactColor.Red);
yield return null;
victim.enabled = false;
neighbor.enabled = false;
Assert.That(victim.ApplyIgnite(3f, 1f, 2f), Is.True);
Assert.That(victim.TryTakeDamage(victim.CurrentHealth + 1f, Vector2.zero, 0f,
false, false, EnemyDamageSource.Player), Is.True);
Assert.That(neighbor.IsIgnited, Is.True);
Assert.That(neighbor.IgniteRemaining, Is.GreaterThan(0f));
}
[UnityTest]
public IEnumerator RiftWedge_ThreeOrdinaryHitsApplyExistingVulnerability()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.RiftWedge), Is.True);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f);
EnemyController target = CreateRelicTestEnemy(origin, ArtifactColor.Red);
yield return null;
target.enabled = false;
CombatHitResult hit = new(passiveRelicPlayer.gameObject, target.gameObject,
false, HitSide.Front, 1f, Vector2.right, 0f, origin, baseBumpDamage: 1f);
CombatEvents.RaiseValidHit(hit);
CombatEvents.RaiseValidHit(hit);
Assert.That(target.IsBumpVulnerable, Is.False);
CombatEvents.RaiseValidHit(hit);
Assert.That(target.IsBumpVulnerable, Is.True);
}
[UnityTest]
public IEnumerator ExecutionerSigil_FinishesLowHealthNonBossAfterOrdinaryHit()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.ExecutionerSigil), Is.True);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f);
EnemyController target = CreateRelicTestEnemy(origin, ArtifactColor.Red);
yield return null;
target.enabled = false;
SetEnemyCurrentHealth(target, target.MaximumHealth * 0.09f);
CombatEvents.RaiseValidHit(new CombatHitResult(passiveRelicPlayer.gameObject,
target.gameObject, false, HitSide.Front, 1f, Vector2.right, 0f,
origin, baseBumpDamage: 1f));
Assert.That(target.IsDead, Is.True);
}
[UnityTest]
public IEnumerator BackflowHeart_GrantsShieldAfterActualHpLossAndAbsorbsNextHit()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.BackflowHeart), Is.True);
PlayerHealth health = passiveRelicPlayer.GetComponent<PlayerHealth>();
SetTestField(health, "invulnerableUntil", Time.time - 1f);
Assert.That(health.TryTakeDamage(health.MaxHealth * 0.3f,
Vector2.right, 0f, fromEnemyAttack: false), Is.True);
Assert.That(health.CurrentShield, Is.Zero);
SetTestField(health, "invulnerableUntil", Time.time - 1f);
Assert.That(health.TryTakeDamage(health.MaxHealth * 0.4f,
Vector2.right, 0f), Is.True);
Assert.That(health.CurrentShield, Is.GreaterThan(0f));
float hpAfter = health.CurrentHealth;
SetTestField(health, "invulnerableUntil", Time.time - 1f);
Assert.That(health.TryTakeDamage(1f, Vector2.right, 0f), Is.True);
Assert.That(health.CurrentHealth, Is.EqualTo(hpAfter));
}
[UnityTest]
public IEnumerator CounterBell_SlowsNearbyEnemyAfterActualHpLoss()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.CounterBell), Is.True);
EnemyController target = CreateRelicTestEnemy(
(Vector2)passiveRelicPlayer.transform.position + Vector2.right,
ArtifactColor.Red);
yield return null;
target.enabled = false;
PlayerHealth health = passiveRelicPlayer.GetComponent<PlayerHealth>();
SetTestField(health, "invulnerableUntil", Time.time - 1f);
Assert.That(health.TryTakeDamage(10f, Vector2.right, 0f), Is.True);
Assert.That(target.MovementSpeedMultiplier, Is.EqualTo(0.7f).Within(0.001f));
}
[UnityTest]
public IEnumerator SprintWheel_ConsumesOneChargeOnNextOrdinaryBump()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.SprintWheel), Is.True);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f);
EnemyController original = CreateRelicTestEnemy(origin, ArtifactColor.Red);
EnemyController neighbor = CreateRelicTestEnemy(origin + Vector2.right, ArtifactColor.Red);
yield return null;
original.enabled = false;
neighbor.enabled = false;
Collider2D playerCollider = passiveRelicPlayer.GetComponent<Collider2D>();
float diameter = Mathf.Max(playerCollider.bounds.size.x, playerCollider.bounds.size.y);
passiveRelicController.AddVoluntaryMovement(diameter *
GameplayConstants.Current.PassiveRelics.SprintWheelRequiredBodyDiameters);
CombatHitResult hit = new(passiveRelicPlayer.gameObject, original.gameObject,
false, HitSide.Front, 10f, Vector2.right, 0f, origin, baseBumpDamage: 10f);
float before = neighbor.CurrentHealth;
CombatEvents.RaiseValidHit(hit);
Assert.That(neighbor.CurrentHealth, Is.LessThan(before));
float after = neighbor.CurrentHealth;
CombatEvents.RaiseValidHit(hit);
Assert.That(neighbor.CurrentHealth, Is.EqualTo(after));
}
[UnityTest]
public IEnumerator ConditionalRelics_RequireIgniteAndOwnedDamagingArtifact()
{
yield return PreparePassiveRelicFixture(null);
List<ActiveArtifactDefinition> owned = typeof(ActiveArtifactController)
.GetField("ownedArtifacts", BindingFlags.Instance | BindingFlags.NonPublic)
?.GetValue(passiveRelicArtifacts) as List<ActiveArtifactDefinition>;
Assert.That(owned, Is.Not.Null);
owned.Clear();
Assert.That(passiveRelicController.GetAvailableRelics(),
Has.None.EqualTo(PassiveRelicId.EmberCarrier));
Assert.That(passiveRelicController.GetAvailableRelics(),
Has.None.EqualTo(PassiveRelicId.ResonantCrystal));
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.EmberWick), Is.True);
Assert.That(passiveRelicController.GetAvailableRelics(),
Has.Some.EqualTo(PassiveRelicId.EmberCarrier));
ActiveArtifactDefinition phoenix = null;
for (int i = 0; i < passiveRelicArtifacts.CatalogCount; i++)
{
ActiveArtifactDefinition definition = passiveRelicArtifacts.GetCatalogArtifactAt(i);
if (definition != null && definition.Effect == ActiveArtifactEffect.Phoenix)
{
phoenix = definition;
break;
}
}
Assert.That(phoenix, Is.Not.Null);
owned.Add(phoenix);
Assert.That(passiveRelicController.GetAvailableRelics(),
Has.Some.EqualTo(PassiveRelicId.ResonantCrystal));
}
[UnityTest]
public IEnumerator HunterFeather_BoostsMoveSpeedAfterPlayerKill()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.HunterFeather), Is.True);
PlayerStats stats = passiveRelicPlayer.GetComponent<PlayerStats>();
float before = stats.MoveSpeed;
EnemyController victim = CreateRelicTestEnemy(
(Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f),
ArtifactColor.Red);
yield return null;
victim.enabled = false;
Assert.That(victim.TryTakeDamage(victim.CurrentHealth + 1f, Vector2.zero,
0f, false, false, EnemyDamageSource.Player), Is.True);
Assert.That(stats.MoveSpeed, Is.GreaterThan(before));
}
[UnityTest]
public IEnumerator BloodFang_HealsOnlyAfterValidHit()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.BloodFang), Is.True);
PlayerHealth health = passiveRelicPlayer.GetComponent<PlayerHealth>();
SetTestField(health, "<CurrentHealth>k__BackingField", health.MaxHealth - 20f);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f);
EnemyController target = CreateRelicTestEnemy(origin, ArtifactColor.Red);
yield return null;
target.enabled = false;
float before = health.CurrentHealth;
CombatEvents.RaiseValidHit(new CombatHitResult(passiveRelicPlayer.gameObject,
target.gameObject, true, HitSide.Front, 1f, Vector2.right, 0f,
origin, baseBumpDamage: 1f));
Assert.That(health.CurrentHealth, Is.GreaterThan(before));
}
[UnityTest]
public IEnumerator RecoveryFunnel_RestoresGaugeFromPlayerCreditedKill()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.RecoveryFunnel), Is.True);
SetTestField(passiveRelicArtifacts, "<CurrentGauge>k__BackingField", 0f);
EnemyController victim = CreateRelicTestEnemy(
(Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f),
ArtifactColor.Red);
yield return null;
victim.enabled = false;
Assert.That(victim.TryTakeDamage(victim.CurrentHealth + 1f, Vector2.zero,
0f, false, false, EnemyDamageSource.Player), Is.True);
Assert.That(passiveRelicArtifacts.CurrentGauge, Is.GreaterThan(0f));
}
[UnityTest]
public IEnumerator PilgrimPouch_CreatesAtMostOneHealingOrb()
{
yield return PreparePassiveRelicFixture(null);
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.PilgrimPouch), Is.True);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f);
EnemyController first = CreateRelicTestEnemy(origin, ArtifactColor.Red);
EnemyController second = CreateRelicTestEnemy(origin + Vector2.right * 5f,
ArtifactColor.Red);
yield return null;
first.enabled = false;
second.enabled = false;
Assert.That(first.TryTakeDamage(first.CurrentHealth + 1f, Vector2.zero,
0f, false, false, EnemyDamageSource.Player), Is.True);
Assert.That(UnityEngine.Object.FindObjectsByType<PassiveRelicHealingOrb>(
FindObjectsInactive.Exclude, FindObjectsSortMode.None).Length, Is.EqualTo(1));
Assert.That(second.TryTakeDamage(second.CurrentHealth + 1f, Vector2.zero,
0f, false, false, EnemyDamageSource.Player), Is.True);
Assert.That(UnityEngine.Object.FindObjectsByType<PassiveRelicHealingOrb>(
FindObjectsInactive.Exclude, FindObjectsSortMode.None).Length, Is.EqualTo(1));
PlayerHealth health = passiveRelicPlayer.GetComponent<PlayerHealth>();
SetTestField(health, "<CurrentHealth>k__BackingField", health.MaxHealth - 20f);
float hpBefore = health.CurrentHealth;
Assert.That(passiveRelicPlayer.TryReposition(origin), Is.True);
yield return null;
Assert.That(health.CurrentHealth, Is.GreaterThan(hpBefore));
}
[UnityTest]
public IEnumerator BattleBaton_BoostsVisibleDamageTowersNotSummons()
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(100), Is.True);
DefenseTower direct = BuildTower(controller, player, 0, DefenseTowerKind.Direct);
DefenseTower summon = BuildTower(controller, player, 1, DefenseTowerKind.Summon);
DefenseTower lightning = BuildTower(controller, player, 4,
DefenseTowerKind.Lightning);
Assert.That(player.TryReposition(summon.transform.position), Is.True);
Assert.That(controller.TryBoostVisibleTowers(3f), Is.False,
"Only a summon tower is currently within sight.");
Assert.That(player.TryReposition(direct.transform.position), Is.True);
Assert.That(controller.TryBoostVisibleTowers(3f), Is.True);
Dictionary<DefenseTower, float> boosted = typeof(DefensePrototypeController)
.GetField("boostedTowers", BindingFlags.Instance | BindingFlags.NonPublic)
?.GetValue(controller) as Dictionary<DefenseTower, float>;
Assert.That(boosted, Is.Not.Null);
Assert.That(boosted.ContainsKey(direct), Is.True);
Assert.That(boosted.ContainsKey(lightning), Is.True);
Assert.That(boosted.ContainsKey(summon), Is.False);
}
[UnityTest]
public IEnumerator BondLedger_AwardsCurrencyForRecentlyBumpedTowerKill()
{
yield return StartDefenseRun();
passiveRelicPlayer = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
passiveRelicController = passiveRelicPlayer.GetComponent<PassiveRelicController>();
Assert.That(passiveRelicController.TryAcquire(PassiveRelicId.BondLedger), Is.True);
#if UNITY_EDITOR
PassiveRelicController.RandomRollForTests = () => 0f;
#endif
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
EnemyController[] prefabs = typeof(SpawnDirector).GetField("enemyPrefabs",
BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(director) as EnemyController[];
Assert.That(prefabs, Is.Not.Null);
foreach (EnemyController prefab in prefabs)
{
if (prefab != null && prefab.Definition != null
&& prefab.Definition.Kind == EnemyKind.Skeleton)
{
passiveRelicEnemyPrefab = prefab;
break;
}
}
Assert.That(passiveRelicEnemyPrefab, Is.Not.Null);
Vector2 origin = (Vector2)passiveRelicPlayer.transform.position + new Vector2(4f, 4f);
EnemyController victim = CreateRelicTestEnemy(origin, ArtifactColor.Red);
yield return null;
victim.enabled = false;
CombatEvents.RaiseValidHit(new CombatHitResult(passiveRelicPlayer.gameObject,
victim.gameObject, false, HitSide.Front, 1f, Vector2.right, 0f,
origin, baseBumpDamage: 1f));
int before = DefensePrototypeController.Instance.ConstructionCurrency;
Assert.That(victim.TryTakeDamage(victim.CurrentHealth + 1f, Vector2.zero,
0f, false, false, EnemyDamageSource.Tower), Is.True);
Assert.That(DefensePrototypeController.Instance.ConstructionCurrency,
Is.EqualTo(before + 1));
}
private PassiveRelicController passiveRelicController;
private ActiveArtifactController passiveRelicArtifacts;
private PlayerController passiveRelicPlayer;