269 lines
8.6 KiB
C#
269 lines
8.6 KiB
C#
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<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;
|
|
}
|
|
|
|
public sealed class PassiveRelicController : MonoBehaviour
|
|
{
|
|
public const string EchoSourceId = "passive_relic_echo";
|
|
|
|
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 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<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;
|
|
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<PlayerController>();
|
|
playerCollider = GetComponent<Collider2D>();
|
|
playerHealth = GetComponent<PlayerHealth>();
|
|
artifacts = GetComponent<ActiveArtifactController>();
|
|
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<EnemyController>();
|
|
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<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 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());
|
|
}
|
|
}
|
|
}
|