using System.Collections.Generic; using System.Linq; using BumpCombat.Combat; using BumpCombat.Constants; using BumpCombat.Enemies; using BumpCombat.Player; using UnityEngine; namespace BumpCombat.Progression { public enum PassiveRelicId { TwinHornTalisman, ResonanceCore, RuptureSeed, } public static class PassiveRelicRules { private static readonly PassiveRelicId[] Catalog = { PassiveRelicId.TwinHornTalisman, PassiveRelicId.ResonanceCore, PassiveRelicId.RuptureSeed, }; public static bool RollsChance(float chance, float roll) => chance > 0f && roll >= 0f && roll < Mathf.Clamp01(chance); public static PassiveRelicId[] GetAvailableRelics( IReadOnlyCollection owned) { List 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; } public sealed class PassiveRelicController : MonoBehaviour { public const string EchoSourceId = "passive_relic_echo"; private readonly HashSet owned = new(); private readonly HashSet processedDeaths = new(); private ActiveArtifactController artifacts; private PlayerController player; private Collider2D playerCollider; private PlayerHealth playerHealth; private bool active; private float nextEchoAllowedAt; public static PassiveRelicController Instance { get; private set; } public int OwnedCount => owned.Count; public bool Owns(PassiveRelicId id) => owned.Contains(id); public int AvailableCount => GetAvailableRelics().Length; #if UNITY_EDITOR public static System.Func 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; CombatEvents.OnValidHit -= HandleValidHit; CombatEvents.OnEnemyDied -= HandleEnemyDied; } private void OnDestroy() { if (Instance == this) { Instance = null; } } public void BeginDefenseRun() { owned.Clear(); processedDeaths.Clear(); nextEchoAllowedAt = 0f; player = GetComponent(); playerCollider = GetComponent(); playerHealth = GetComponent(); artifacts = GetComponent(); active = true; } public void EndDefenseRun() { active = false; } public PassiveRelicId[] GetAvailableRelics() => PassiveRelicRules.GetAvailableRelics(owned); public bool TryAcquire(PassiveRelicId id) { if (!active || !System.Array.Exists(GetAvailableRelics(), candidate => candidate == id)) { return false; } return owned.Add(id); } 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 || !owned.Contains(PassiveRelicId.ResonanceCore) || hit.Attacker != gameObject || hit.Damage <= 0f || playerHealth == null || playerHealth.CurrentHealth <= 0f || hit.SourceId == EchoSourceId || Time.time < nextEchoAllowedAt || !RollsChance(GameplayConstants.Current.PassiveRelics.ResonanceEchoChance) || artifacts == null) { return; } if (artifacts.TryEmitPassiveRelicEcho( hit.HitPosition, hit.KnockbackDirection, GameplayConstants.Current.PassiveRelics.ResonanceEchoDamageMultiplier)) { nextEchoAllowedAt = Time.time + GameplayConstants.Current.PassiveRelics.ResonanceEchoCooldownSeconds; } } private void HandleEnemyDied(GameObject target) { if (!active || !owned.Contains(PassiveRelicId.RuptureSeed) || target == null) { return; } EnemyController victim = target.GetComponent(); if (victim == null || !PassiveRelicRules.IsPlayerCreditedKill(victim.LastDamageSource) || !processedDeaths.Add(victim.SpawnLifetimeIdentity) || !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 || playerHealth == null || playerHealth.CurrentHealth <= 0f) { return; } Collider2D[] hits = Physics2D.OverlapCircleAll(victim.transform.position, radius); HashSet damagedEnemies = new(); foreach (Collider2D hit in hits) { EnemyController enemy = hit != null ? hit.GetComponentInParent() : 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 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()); } } }