Files

1098 lines
37 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Defense;
using BumpCombat.Enemies;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Progression
{
public enum PassiveRelicId
{
TwinHornTalisman,
ResonanceCore,
RuptureSeed,
WaveNail,
CapacitorRing,
EmberWick,
EmberCarrier,
RiftWedge,
ExecutionerSigil,
HunterFeather,
BloodFang,
RecoveryFunnel,
TrophyMask,
ResonantCrystal,
BackflowHeart,
CounterBell,
SprintWheel,
BattleBaton,
BondLedger,
PilgrimPouch,
}
public static class PassiveRelicRules
{
private static readonly PassiveRelicId[] Catalog =
{
PassiveRelicId.TwinHornTalisman,
PassiveRelicId.ResonanceCore,
PassiveRelicId.RuptureSeed,
PassiveRelicId.WaveNail,
PassiveRelicId.CapacitorRing,
PassiveRelicId.EmberWick,
PassiveRelicId.EmberCarrier,
PassiveRelicId.RiftWedge,
PassiveRelicId.ExecutionerSigil,
PassiveRelicId.HunterFeather,
PassiveRelicId.BloodFang,
PassiveRelicId.RecoveryFunnel,
PassiveRelicId.TrophyMask,
PassiveRelicId.ResonantCrystal,
PassiveRelicId.BackflowHeart,
PassiveRelicId.CounterBell,
PassiveRelicId.SprintWheel,
PassiveRelicId.BattleBaton,
PassiveRelicId.BondLedger,
PassiveRelicId.PilgrimPouch,
};
public static bool RollsChance(float chance, float roll) =>
chance > 0f && roll >= 0f && roll < Mathf.Clamp01(chance);
public static PassiveRelicId[] GetAvailableRelics(
IReadOnlyCollection<PassiveRelicId> owned)
{
List<PassiveRelicId> available = new(Catalog.Length);
foreach (PassiveRelicId id in Catalog)
{
if (owned == null || !owned.Contains(id))
{
available.Add(id);
}
}
return available.ToArray();
}
public static float ApplyTwinHornDamage(
float damage,
bool ownsRelic,
bool isOrdinaryBump,
bool hasArtifactColor,
float chance,
float roll)
{
return ownsRelic
&& isOrdinaryBump
&& !hasArtifactColor
&& RollsChance(chance, roll)
? damage * 2f
: damage;
}
public static float GetExplosionDamage(float maximumHealth, float fraction) =>
Mathf.Max(0f, maximumHealth) * Mathf.Clamp01(fraction);
public static float GetExplosionRadius(float bodyDiameter, float diameterMultiplier) =>
Mathf.Max(0f, bodyDiameter) * Mathf.Max(0f, diameterMultiplier);
public static bool IsPlayerCreditedKill(EnemyDamageSource source) =>
source == EnemyDamageSource.Player
|| source == EnemyDamageSource.PassiveRelicAttack;
public static bool IsPlayerRewardKill(EnemyDamageSource source) =>
source == EnemyDamageSource.Player
|| source == EnemyDamageSource.PassiveRelicExplosion
|| source == EnemyDamageSource.PassiveRelicAttack
|| source == EnemyDamageSource.PassiveRelicTransferredIgnite;
}
public sealed class PassiveRelicController : MonoBehaviour
{
public const string EchoSourceId = "passive_relic_echo";
private const string HunterBuffSourceId = "relic.hunter-feather";
private const string TrophyBuffSourceId = "relic.trophy-mask";
private readonly HashSet<PassiveRelicId> owned = new();
private readonly HashSet<int> processedDeaths = new();
private ActiveArtifactController artifacts;
private PlayerController player;
private Collider2D playerCollider;
private PlayerHealth playerHealth;
private PlayerStats playerStats;
private float nextBloodAllowedAt;
private float nextRecoveryAllowedAt;
private float hunterExpiresAt;
private float trophyExpiresAt;
private float heartAccumulatedDamage;
private float nextHeartAllowedAt;
private float nextBellAllowedAt;
private float nextRepeatAllowedAt;
private bool repeatPending;
private float voluntaryMovementDistance;
private bool wheelCharged;
private float nextBatonAllowedAt;
private float nextLedgerAllowedAt;
private float nextPouchAllowedAt;
private readonly Dictionary<int, float> recentBumps = new();
private PassiveRelicHealingOrb healingOrb;
private bool active;
private float nextEchoAllowedAt;
private float nextWaveAllowedAt;
private float nextChainAllowedAt;
private float nextCarrierAllowedAt;
private readonly Dictionary<int, (int hits, float lastHitAt)> riftHits = new();
public static PassiveRelicController Instance { get; private set; }
public int OwnedCount => owned.Count;
public bool IsRunActive => active;
public bool Owns(PassiveRelicId id) => owned.Contains(id);
public int AvailableCount => GetAvailableRelics().Length;
#if UNITY_EDITOR
public static System.Func<float> RandomRollForTests { get; set; }
#endif
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(this);
return;
}
Instance = this;
}
private void OnEnable()
{
CombatEvents.OnValidHit += HandleValidHit;
CombatEvents.OnEnemyDied += HandleEnemyDied;
}
private void OnDisable()
{
active = false;
if (playerHealth != null)
{
playerHealth.OnEnemyDamageTaken -= HandleDamageTaken;
}
if (artifacts != null)
{
artifacts.OnArtifactUseSucceeded -= HandleArtifactUseSucceeded;
}
repeatPending = false;
CombatEvents.OnValidHit -= HandleValidHit;
CombatEvents.OnEnemyDied -= HandleEnemyDied;
}
private void OnDestroy()
{
if (Instance == this)
{
Instance = null;
}
}
public void BeginDefenseRun()
{
owned.Clear();
processedDeaths.Clear();
nextEchoAllowedAt = 0f;
nextWaveAllowedAt = 0f;
nextChainAllowedAt = 0f;
nextCarrierAllowedAt = 0f;
riftHits.Clear();
recentBumps.Clear();
voluntaryMovementDistance = 0f;
wheelCharged = false;
nextBatonAllowedAt = 0f;
nextLedgerAllowedAt = 0f;
nextPouchAllowedAt = 0f;
if (healingOrb != null)
{
Destroy(healingOrb.gameObject);
healingOrb = null;
}
player = GetComponent<PlayerController>();
player?.SetPassiveRelicController(this);
playerCollider = GetComponent<Collider2D>();
playerHealth = GetComponent<PlayerHealth>();
playerStats = GetComponent<PlayerStats>();
nextBloodAllowedAt = 0f;
nextRecoveryAllowedAt = 0f;
ClearTemporaryBuffs();
heartAccumulatedDamage = 0f;
nextHeartAllowedAt = 0f;
nextBellAllowedAt = 0f;
playerHealth.OnEnemyDamageTaken -= HandleDamageTaken;
playerHealth.OnEnemyDamageTaken += HandleDamageTaken;
artifacts = GetComponent<ActiveArtifactController>();
if (artifacts != null)
{
artifacts.OnArtifactUseSucceeded -= HandleArtifactUseSucceeded;
artifacts.OnArtifactUseSucceeded += HandleArtifactUseSucceeded;
}
nextRepeatAllowedAt = 0f;
repeatPending = false;
active = true;
}
public void EndDefenseRun()
{
active = false;
StopAllCoroutines();
repeatPending = false;
if (healingOrb != null)
{
Destroy(healingOrb.gameObject);
healingOrb = null;
}
if (artifacts != null)
{
artifacts.OnArtifactUseSucceeded -= HandleArtifactUseSucceeded;
}
if (playerHealth != null)
{
playerHealth.OnEnemyDamageTaken -= HandleDamageTaken;
}
ClearTemporaryBuffs();
}
public PassiveRelicId[] GetAvailableRelics() =>
PassiveRelicRules.GetAvailableRelics(owned)
.Where(CanOfferRelic)
.ToArray();
private bool CanOfferRelic(PassiveRelicId id)
{
if (id == PassiveRelicId.EmberCarrier)
{
ActiveArtifactDefinition phoenix = FindArtifactDefinition(
ActiveArtifactEffect.Phoenix);
return owned.Contains(PassiveRelicId.EmberWick)
|| (phoenix != null
&& artifacts != null
&& artifacts.OwnsArtifact(phoenix));
}
if (id == PassiveRelicId.ResonantCrystal)
{
if (artifacts == null)
{
return false;
}
for (int i = 0; i < artifacts.OwnedArtifactCount; i++)
{
ActiveArtifactDefinition definition = artifacts.GetArtifactAt(i);
if (definition != null
&& (definition.NormalDamage > 0f
|| definition.ChargedDamage > 0f))
{
return true;
}
}
return false;
}
return true;
}
public bool TryAcquire(PassiveRelicId id)
{
if (!active || !System.Array.Exists(GetAvailableRelics(), candidate => candidate == id))
{
return false;
}
return owned.Add(id);
}
public void AddVoluntaryMovement(float actualDistance)
{
if (!active
|| !owned.Contains(PassiveRelicId.SprintWheel)
|| wheelCharged
|| actualDistance <= 0f
|| playerHealth == null
|| playerHealth.CurrentHealth <= 0f)
{
return;
}
float diameter = playerCollider != null
? Mathf.Max(playerCollider.bounds.size.x, playerCollider.bounds.size.y)
: 0f;
if (diameter <= 0f)
{
return;
}
voluntaryMovementDistance += actualDistance;
wheelCharged = voluntaryMovementDistance >= diameter
* GameplayConstants.Current.PassiveRelics.SprintWheelRequiredBodyDiameters;
}
public float ApplyTrophyBumpDamage(float damage, bool ordinaryBump)
{
return active
&& ordinaryBump
&& Time.time < trophyExpiresAt
? damage * (1f + GameplayConstants.Current.PassiveRelics.TrophyMaskBumpDamageIncrease)
: damage;
}
public float ApplyOrdinaryBumpDamageMultiplier(
float damage,
bool isOrdinaryBump,
bool hasArtifactColor)
{
if (!active || !owned.Contains(PassiveRelicId.TwinHornTalisman))
{
return damage;
}
return PassiveRelicRules.ApplyTwinHornDamage(
damage,
true,
isOrdinaryBump,
hasArtifactColor,
GameplayConstants.Current.PassiveRelics.TwinHornDoubleDamageChance,
GetRandomRoll());
}
private void HandleValidHit(CombatHitResult hit)
{
if (!active
|| hit.Attacker != gameObject
|| hit.Target == null
|| hit.Damage <= 0f
|| playerHealth == null
|| playerHealth.CurrentHealth <= 0f
|| hit.SourceId == EchoSourceId)
{
return;
}
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
if (hit.IsOrdinaryBump && !hit.IsArtifactHit)
{
EnemyController bumped = hit.Target.GetComponentInParent<EnemyController>();
if (bumped != null && owned.Contains(PassiveRelicId.BondLedger))
{
recentBumps[bumped.SpawnLifetimeIdentity] = Time.time;
}
TrySprintWheel(hit, tuning);
}
if (hit.BaseBumpDamage > 0f)
{
TryWaveNail(hit, tuning);
TryCapacitorRing(hit, tuning);
}
TryEmberWick(hit, tuning);
TryRiftWedge(hit, tuning);
TryExecutionerSigil(hit, tuning);
TryBloodFang(tuning);
if (!owned.Contains(PassiveRelicId.ResonanceCore)
|| Time.time < nextEchoAllowedAt
|| !RollsChance(tuning.ResonanceEchoChance)
|| artifacts == null)
{
return;
}
if (artifacts.TryEmitPassiveRelicEcho(
hit.HitPosition,
hit.KnockbackDirection,
tuning.ResonanceEchoDamageMultiplier))
{
nextEchoAllowedAt = Time.time
+ tuning.ResonanceEchoCooldownSeconds;
}
}
private void TryWaveNail(CombatHitResult hit, PassiveRelicConstants tuning)
{
if (!owned.Contains(PassiveRelicId.WaveNail)
|| Time.time < nextWaveAllowedAt
|| !RollsChance(tuning.WaveNailChance))
{
return;
}
float damage = hit.BaseBumpDamage * tuning.WaveNailDamageFraction;
if (damage <= 0f)
{
return;
}
bool applied = false;
foreach (EnemyController enemy in FindNearbyEnemies(hit.HitPosition))
{
applied |= TryApplyRelicDamage(enemy, hit.HitPosition, damage);
}
if (applied)
{
nextWaveAllowedAt = Time.time + tuning.WaveNailCooldownSeconds;
}
}
private void TryCapacitorRing(CombatHitResult hit, PassiveRelicConstants tuning)
{
if (!owned.Contains(PassiveRelicId.CapacitorRing)
|| Time.time < nextChainAllowedAt
|| !RollsChance(tuning.CapacitorRingChance))
{
return;
}
float damage = hit.BaseBumpDamage * tuning.CapacitorRingDamageFraction;
if (damage <= 0f)
{
return;
}
bool applied = false;
int hitCount = 0;
foreach (EnemyController enemy in FindNearbyEnemies(hit.HitPosition)
.OrderBy(enemy => ((Vector2)enemy.transform.position - hit.HitPosition).sqrMagnitude))
{
if (enemy.gameObject == hit.Target || hitCount >= tuning.CapacitorRingMaxTargets)
{
continue;
}
if (TryApplyRelicDamage(enemy, hit.HitPosition, damage))
{
applied = true;
hitCount++;
}
}
if (applied)
{
nextChainAllowedAt = Time.time + tuning.CapacitorRingCooldownSeconds;
}
}
private void TryEmberWick(CombatHitResult hit, PassiveRelicConstants tuning)
{
if (!owned.Contains(PassiveRelicId.EmberWick)
|| !RollsChance(tuning.EmberWickChance)
|| artifacts == null)
{
return;
}
EnemyController enemy = hit.Target.GetComponentInParent<EnemyController>();
if (enemy == null || enemy.IsDead)
{
return;
}
ActiveArtifactDefinition phoenix = FindArtifactDefinition(
ActiveArtifactEffect.Phoenix);
if (phoenix != null)
{
enemy.ApplyIgnite(
phoenix.NormalIgniteDuration,
phoenix.NormalIgniteInterval,
playerStats.CalculateDamage(
phoenix.NormalIgniteTickDamage,
phoenix.DamageTag));
}
}
private List<EnemyController> FindNearbyEnemies(Vector2 position)
{
float diameter = playerCollider != null
? Mathf.Max(playerCollider.bounds.size.x, playerCollider.bounds.size.y)
: 0f;
float radius = PassiveRelicRules.GetExplosionRadius(diameter, 3f);
List<EnemyController> nearby = new();
HashSet<EnemyController> seen = new();
foreach (Collider2D collider in Physics2D.OverlapCircleAll(position, radius))
{
EnemyController enemy = collider != null
? collider.GetComponentInParent<EnemyController>()
: null;
if (enemy != null && !enemy.IsDead && seen.Add(enemy))
{
nearby.Add(enemy);
}
}
return nearby;
}
private static bool TryApplyRelicDamage(EnemyController enemy, Vector2 origin, float damage)
{
Vector2 direction = ((Vector2)enemy.transform.position - origin).normalized;
return enemy.TryTakeDamage(
damage,
direction,
0f,
true,
false,
EnemyDamageSource.PassiveRelicAttack);
}
private void TrySprintWheel(CombatHitResult hit, PassiveRelicConstants tuning)
{
if (!wheelCharged || hit.BaseBumpDamage <= 0f)
{
return;
}
wheelCharged = false;
voluntaryMovementDistance = 0f;
float damage = hit.BaseBumpDamage
* tuning.SprintWheelShockwaveDamageFraction;
foreach (EnemyController enemy in FindNearbyEnemies(hit.HitPosition))
{
TryApplyRelicDamage(enemy, hit.HitPosition, damage);
}
}
private void TryRiftWedge(CombatHitResult hit, PassiveRelicConstants tuning)
{
if (!owned.Contains(PassiveRelicId.RiftWedge)
|| !hit.IsOrdinaryBump
|| hit.IsArtifactHit)
{
return;
}
EnemyController enemy = hit.Target.GetComponentInParent<EnemyController>();
if (enemy == null || enemy.IsDead)
{
return;
}
int key = enemy.SpawnLifetimeIdentity;
riftHits.TryGetValue(key, out var progress);
int count = Time.time - progress.lastHitAt > tuning.RiftWedgeMaxHitGapSeconds
? 1
: progress.hits + 1;
if (count < tuning.RiftWedgeRequiredHits)
{
riftHits[key] = (count, Time.time);
return;
}
riftHits.Remove(key);
ActiveArtifactDefinition thunder = FindArtifactDefinition(
ActiveArtifactEffect.ThunderCrash);
if (thunder != null)
{
enemy.ApplyBumpVulnerability(
thunder.NormalVulnerabilityDuration,
thunder.NormalVulnerabilityIncrease);
}
}
private void TryExecutionerSigil(CombatHitResult hit, PassiveRelicConstants tuning)
{
if (!owned.Contains(PassiveRelicId.ExecutionerSigil)
|| !hit.IsOrdinaryBump
|| hit.IsArtifactHit
|| !RollsChance(tuning.ExecutionerSigilChance))
{
return;
}
EnemyController enemy = hit.Target.GetComponentInParent<EnemyController>();
if (enemy == null
|| enemy.IsDead
|| enemy.GroggyTier == BumpCombat.Core.RunTimedEvent.MidBoss
|| enemy.GroggyTier == BumpCombat.Core.RunTimedEvent.FinalBoss
|| enemy.CurrentHealth > enemy.MaximumHealth
* tuning.ExecutionerSigilHealthThreshold)
{
return;
}
enemy.TryTakeDamage(
enemy.MaximumHealth * 100f,
hit.KnockbackDirection,
0f,
true,
false,
EnemyDamageSource.PassiveRelicAttack);
}
private ActiveArtifactDefinition FindArtifactDefinition(ActiveArtifactEffect effect)
{
if (artifacts == null)
{
return null;
}
for (int i = 0; i < artifacts.CatalogCount; i++)
{
ActiveArtifactDefinition definition = artifacts.GetCatalogArtifactAt(i);
if (definition != null && definition.Effect == effect)
{
return definition;
}
}
return null;
}
private void HandleEnemyDied(GameObject target)
{
if (!active || target == null)
{
return;
}
EnemyController victim = target.GetComponent<EnemyController>();
if (victim == null
|| !processedDeaths.Add(victim.SpawnLifetimeIdentity)
|| playerHealth == null
|| playerHealth.CurrentHealth <= 0f)
{
return;
}
riftHits.Remove(victim.SpawnLifetimeIdentity);
TryBondLedger(victim);
recentBumps.Remove(victim.SpawnLifetimeIdentity);
if (!PassiveRelicRules.IsPlayerRewardKill(victim.LastDamageSource))
{
return;
}
TryHunterFeather();
TryRecoveryFunnel();
TryTrophyMask(victim);
TryPilgrimPouch(victim);
if (!PassiveRelicRules.IsPlayerCreditedKill(victim.LastDamageSource))
{
return;
}
TryEmberCarrier(victim);
if (!owned.Contains(PassiveRelicId.RuptureSeed)
|| !RollsChance(GameplayConstants.Current.PassiveRelics.RuptureSeedChance))
{
return;
}
float damage = PassiveRelicRules.GetExplosionDamage(
victim.MaximumHealthAtDeath,
GameplayConstants.Current.PassiveRelics.RuptureExplosionMaxHealthFraction);
float diameter = playerCollider != null
? Mathf.Max(playerCollider.bounds.size.x, playerCollider.bounds.size.y)
: 0f;
float radius = PassiveRelicRules.GetExplosionRadius(
diameter,
GameplayConstants.Current.PassiveRelics.RuptureExplosionPlayerBodyDiameters);
if (damage <= 0f || radius <= 0f)
{
return;
}
Collider2D[] hits = Physics2D.OverlapCircleAll(victim.transform.position, radius);
HashSet<EnemyController> damagedEnemies = new();
foreach (Collider2D hit in hits)
{
EnemyController enemy = hit != null ? hit.GetComponentInParent<EnemyController>() : null;
if (enemy == null
|| enemy == victim
|| enemy.IsDead
|| !damagedEnemies.Add(enemy))
{
continue;
}
Vector2 direction = ((Vector2)enemy.transform.position - (Vector2)victim.transform.position).normalized;
enemy.TryTakeDamage(
damage,
direction,
0f,
true,
false,
EnemyDamageSource.PassiveRelicExplosion);
}
}
private void Update()
{
if (playerStats == null)
{
return;
}
if (!active || playerHealth == null || playerHealth.CurrentHealth <= 0f)
{
ClearTemporaryBuffs();
return;
}
if (hunterExpiresAt > 0f && Time.time >= hunterExpiresAt)
{
playerStats.RemoveModifiersFromSource(HunterBuffSourceId);
hunterExpiresAt = 0f;
}
if (trophyExpiresAt > 0f && Time.time >= trophyExpiresAt)
{
playerStats.RemoveModifiersFromSource(TrophyBuffSourceId);
trophyExpiresAt = 0f;
}
}
private void ClearTemporaryBuffs()
{
playerStats?.RemoveModifiersFromSource(HunterBuffSourceId);
playerStats?.RemoveModifiersFromSource(TrophyBuffSourceId);
hunterExpiresAt = 0f;
trophyExpiresAt = 0f;
}
private void TryBloodFang(PassiveRelicConstants tuning)
{
if (!owned.Contains(PassiveRelicId.BloodFang)
|| Time.time < nextBloodAllowedAt
|| playerHealth.CurrentHealth >= playerHealth.MaxHealth
|| !RollsChance(tuning.BloodFangChance))
{
return;
}
float before = playerHealth.CurrentHealth;
playerHealth.Heal(playerHealth.MaxHealth * tuning.BloodFangHealMaxHealthFraction);
if (playerHealth.CurrentHealth > before)
{
nextBloodAllowedAt = Time.time + tuning.BloodFangCooldownSeconds;
}
}
private void TryHunterFeather()
{
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
if (!owned.Contains(PassiveRelicId.HunterFeather)
|| !RollsChance(tuning.HunterFeatherChance))
{
return;
}
if (hunterExpiresAt <= Time.time)
{
playerStats.AddModifier(new StatModifier(
HunterBuffSourceId,
CharacterStat.MoveSpeed,
ModifierOperation.Increased,
tuning.HunterFeatherMoveSpeedIncrease));
}
hunterExpiresAt = Time.time + tuning.HunterFeatherDurationSeconds;
}
private void TryRecoveryFunnel()
{
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
if (!owned.Contains(PassiveRelicId.RecoveryFunnel)
|| Time.time < nextRecoveryAllowedAt
|| artifacts == null
|| !RollsChance(tuning.RecoveryFunnelChance))
{
return;
}
if (artifacts.TryRestorePassiveRelicGauge(
tuning.RecoveryFunnelGaugeFraction))
{
nextRecoveryAllowedAt = Time.time
+ tuning.RecoveryFunnelCooldownSeconds;
}
}
private void TryTrophyMask(EnemyController victim)
{
if (!owned.Contains(PassiveRelicId.TrophyMask)
|| victim.GroggyTier != RunTimedEvent.Elite)
{
return;
}
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
if (trophyExpiresAt <= Time.time)
{
playerStats.AddModifier(new StatModifier(
TrophyBuffSourceId,
CharacterStat.MoveSpeed,
ModifierOperation.Increased,
tuning.TrophyMaskMoveSpeedIncrease));
}
trophyExpiresAt = Time.time + tuning.TrophyMaskDurationSeconds;
}
private void TryBattleBaton(PassiveRelicConstants tuning)
{
if (!active
|| !owned.Contains(PassiveRelicId.BattleBaton)
|| Time.time < nextBatonAllowedAt)
{
return;
}
if (DefensePrototypeController.Instance?.TryBoostVisibleTowers(
tuning.BattleBatonDurationSeconds) == true)
{
nextBatonAllowedAt = Time.time
+ tuning.BattleBatonCooldownSeconds;
}
}
private void TryBondLedger(EnemyController victim)
{
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
if (!owned.Contains(PassiveRelicId.BondLedger)
|| victim.LastDamageSource != EnemyDamageSource.Tower
|| Time.time < nextLedgerAllowedAt
|| !recentBumps.TryGetValue(
victim.SpawnLifetimeIdentity,
out float bumpedAt)
|| Time.time - bumpedAt > tuning.BondLedgerBumpWindowSeconds
|| !RollsChance(tuning.BondLedgerChance))
{
return;
}
if (DefensePrototypeController.Instance?.TryAwardBondLedgerCurrency(
tuning.BondLedgerCurrency) == true)
{
nextLedgerAllowedAt = Time.time
+ tuning.BondLedgerCooldownSeconds;
}
}
private void TryPilgrimPouch(EnemyController victim)
{
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
if (!owned.Contains(PassiveRelicId.PilgrimPouch)
|| healingOrb != null
|| Time.time < nextPouchAllowedAt
|| !RollsChance(tuning.PilgrimPouchChance))
{
return;
}
GameObject orbObject = new("Pilgrim Healing Orb");
orbObject.transform.position = victim.transform.position;
healingOrb = orbObject.AddComponent<PassiveRelicHealingOrb>();
healingOrb.Initialize(
this,
playerHealth,
tuning.PilgrimPouchHealMaxHealthFraction,
tuning.PilgrimPouchOrbLifetimeSeconds);
nextPouchAllowedAt = Time.time + tuning.PilgrimPouchCooldownSeconds;
}
private void HandleArtifactUseSucceeded(
ActiveArtifactDefinition definition,
bool charged)
{
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
TryBattleBaton(tuning);
if (!active
|| !owned.Contains(PassiveRelicId.ResonantCrystal)
|| repeatPending
|| Time.time < nextRepeatAllowedAt
|| playerHealth == null
|| playerHealth.CurrentHealth <= 0f
|| !RollsChance(tuning.ResonantCrystalChance))
{
return;
}
Vector2 origin = artifacts.GetPassiveRelicUseOrigin();
Vector2 direction = artifacts.GetPassiveRelicUseDirection();
repeatPending = true;
StartCoroutine(RepeatArtifactAfterDelay(
definition,
charged,
origin,
direction,
tuning));
}
private IEnumerator RepeatArtifactAfterDelay(
ActiveArtifactDefinition definition,
bool charged,
Vector2 origin,
Vector2 direction,
PassiveRelicConstants tuning)
{
yield return new WaitForSeconds(tuning.ResonantCrystalDelaySeconds);
repeatPending = false;
if (!active
|| playerHealth == null
|| playerHealth.CurrentHealth <= 0f
|| artifacts == null)
{
yield break;
}
if (artifacts.TryEmitPassiveRelicRepeat(
definition,
charged,
origin,
direction,
tuning.ResonantCrystalDamageFraction))
{
nextRepeatAllowedAt = Time.time
+ tuning.ResonantCrystalCooldownSeconds;
}
}
private void HandleDamageTaken(float actualLoss)
{
if (!active
|| actualLoss <= 0f
|| playerHealth == null
|| playerHealth.CurrentHealth <= 0f)
{
return;
}
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
if (owned.Contains(PassiveRelicId.BackflowHeart)
&& Time.time >= nextHeartAllowedAt)
{
heartAccumulatedDamage += actualLoss;
if (heartAccumulatedDamage >= playerHealth.MaxHealth
* tuning.BackflowHeartDamageThreshold)
{
heartAccumulatedDamage = 0f;
if (playerHealth.GrantPassiveRelicShield(
playerHealth.MaxHealth * tuning.BackflowHeartShieldFraction,
tuning.BackflowHeartShieldDurationSeconds))
{
nextHeartAllowedAt = Time.time
+ tuning.BackflowHeartCooldownSeconds;
}
}
}
if (!owned.Contains(PassiveRelicId.CounterBell)
|| Time.time < nextBellAllowedAt
|| !RollsChance(tuning.CounterBellChance))
{
return;
}
bool applied = false;
foreach (EnemyController enemy in FindNearbyEnemies(transform.position))
{
applied |= enemy.ApplySlow(
tuning.CounterBellSlowDurationSeconds,
tuning.CounterBellSlowFraction);
}
if (applied)
{
nextBellAllowedAt = Time.time + tuning.CounterBellCooldownSeconds;
}
}
private void TryEmberCarrier(EnemyController victim)
{
PassiveRelicConstants tuning = GameplayConstants.Current.PassiveRelics;
if (!owned.Contains(PassiveRelicId.EmberCarrier)
|| victim.IgniteRemainingAtDeath <= 0f
|| victim.IgniteTickIntervalAtDeath <= 0f
|| victim.IgniteTickDamageAtDeath <= 0f
|| Time.time < nextCarrierAllowedAt
|| !RollsChance(tuning.EmberCarrierChance))
{
return;
}
int count = 0;
foreach (EnemyController enemy in FindNearbyEnemies(victim.transform.position)
.OrderBy(enemy => ((Vector2)enemy.transform.position - (Vector2)victim.transform.position).sqrMagnitude))
{
if (enemy == victim || count >= tuning.EmberCarrierMaxTargets)
{
continue;
}
if (enemy.ApplyIgnite(
victim.IgniteRemainingAtDeath,
victim.IgniteTickIntervalAtDeath,
victim.IgniteTickDamageAtDeath,
EnemyDamageSource.PassiveRelicTransferredIgnite))
{
count++;
}
}
if (count > 0)
{
nextCarrierAllowedAt = Time.time + tuning.EmberCarrierCooldownSeconds;
}
}
private static float GetRandomRoll()
{
#if UNITY_EDITOR
if (RandomRollForTests != null)
{
return RandomRollForTests();
}
#endif
return Random.value;
}
private static bool RollsChance(float chance)
{
return PassiveRelicRules.RollsChance(chance, GetRandomRoll());
}
}
public sealed class PassiveRelicHealingOrb : MonoBehaviour
{
private PassiveRelicController owner;
private PlayerHealth playerHealth;
private float healFraction;
private float expiresAt;
public void Initialize(
PassiveRelicController relicOwner,
PlayerHealth health,
float fraction,
float lifetime)
{
owner = relicOwner;
playerHealth = health;
healFraction = fraction;
expiresAt = Time.time + lifetime;
SpriteRenderer renderer = gameObject.AddComponent<SpriteRenderer>();
renderer.sprite = DefensePrototypeController.WhiteSprite;
renderer.color = new Color(0.35f, 1f, 0.55f);
renderer.sortingOrder = 12;
transform.localScale = Vector3.one * 0.3f;
}
private void Update()
{
if (owner == null || !owner.IsRunActive || Time.time >= expiresAt)
{
Destroy(gameObject);
return;
}
if (playerHealth == null
|| playerHealth.CurrentHealth <= 0f
|| Vector2.Distance(
transform.position,
playerHealth.transform.position)
> GameplayConstants.Current.Items.ExperienceOrbPickupRadius)
{
return;
}
playerHealth.Heal(playerHealth.MaxHealth * healFraction);
Destroy(gameObject);
}
}}