Files
tiny_tackle_heroes/Assets/_Project/Scripts/Progression/PassiveRelicController.cs
T
2026-09-23 12:53:52 +09:00

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());
}
}
}