Implement passive relics and six-family defense towers
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
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;
|
||||
@@ -13,6 +16,23 @@ namespace BumpCombat.Progression
|
||||
TwinHornTalisman,
|
||||
ResonanceCore,
|
||||
RuptureSeed,
|
||||
WaveNail,
|
||||
CapacitorRing,
|
||||
EmberWick,
|
||||
EmberCarrier,
|
||||
RiftWedge,
|
||||
ExecutionerSigil,
|
||||
HunterFeather,
|
||||
BloodFang,
|
||||
RecoveryFunnel,
|
||||
TrophyMask,
|
||||
ResonantCrystal,
|
||||
BackflowHeart,
|
||||
CounterBell,
|
||||
SprintWheel,
|
||||
BattleBaton,
|
||||
BondLedger,
|
||||
PilgrimPouch,
|
||||
}
|
||||
|
||||
public static class PassiveRelicRules
|
||||
@@ -22,6 +42,23 @@ namespace BumpCombat.Progression
|
||||
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) =>
|
||||
@@ -64,12 +101,21 @@ namespace BumpCombat.Progression
|
||||
Mathf.Max(0f, bodyDiameter) * Mathf.Max(0f, diameterMultiplier);
|
||||
|
||||
public static bool IsPlayerCreditedKill(EnemyDamageSource source) =>
|
||||
source == EnemyDamageSource.Player;
|
||||
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();
|
||||
@@ -77,11 +123,33 @@ namespace BumpCombat.Progression
|
||||
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;
|
||||
|
||||
@@ -109,11 +177,20 @@ namespace BumpCombat.Progression
|
||||
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()
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
@@ -126,20 +203,104 @@ private void OnDestroy()
|
||||
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);
|
||||
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)
|
||||
{
|
||||
@@ -151,6 +312,40 @@ private void OnDestroy()
|
||||
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,
|
||||
@@ -172,14 +367,39 @@ private void OnDestroy()
|
||||
private void HandleValidHit(CombatHitResult hit)
|
||||
{
|
||||
if (!active
|
||||
|| !owned.Contains(PassiveRelicId.ResonanceCore)
|
||||
|| hit.Attacker != gameObject
|
||||
|| hit.Target == null
|
||||
|| hit.Damage <= 0f
|
||||
|| playerHealth == null
|
||||
|| playerHealth.CurrentHealth <= 0f
|
||||
|| hit.SourceId == EchoSourceId
|
||||
|| 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(GameplayConstants.Current.PassiveRelics.ResonanceEchoChance)
|
||||
|| !RollsChance(tuning.ResonanceEchoChance)
|
||||
|| artifacts == null)
|
||||
{
|
||||
return;
|
||||
@@ -188,24 +408,272 @@ private void OnDestroy()
|
||||
if (artifacts.TryEmitPassiveRelicEcho(
|
||||
hit.HitPosition,
|
||||
hit.KnockbackDirection,
|
||||
GameplayConstants.Current.PassiveRelics.ResonanceEchoDamageMultiplier))
|
||||
tuning.ResonanceEchoDamageMultiplier))
|
||||
{
|
||||
nextEchoAllowedAt = Time.time
|
||||
+ GameplayConstants.Current.PassiveRelics.ResonanceEchoCooldownSeconds;
|
||||
+ 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 || !owned.Contains(PassiveRelicId.RuptureSeed) || target == null)
|
||||
if (!active || target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyController victim = target.GetComponent<EnemyController>();
|
||||
if (victim == null
|
||||
|| !PassiveRelicRules.IsPlayerCreditedKill(victim.LastDamageSource)
|
||||
|| !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;
|
||||
@@ -220,7 +688,7 @@ private void OnDestroy()
|
||||
float radius = PassiveRelicRules.GetExplosionRadius(
|
||||
diameter,
|
||||
GameplayConstants.Current.PassiveRelics.RuptureExplosionPlayerBodyDiameters);
|
||||
if (damage <= 0f || radius <= 0f || playerHealth == null || playerHealth.CurrentHealth <= 0f)
|
||||
if (damage <= 0f || radius <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -249,6 +717,321 @@ private void OnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -262,7 +1045,53 @@ private void OnDestroy()
|
||||
|
||||
private static bool RollsChance(float chance)
|
||||
{
|
||||
return PassiveRelicRules.RollsChance(chance, GetRandomRoll());
|
||||
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);
|
||||
}
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user