using System.Collections.Generic; using BumpCombat.Combat; using BumpCombat.Core; using BumpCombat.Constants; using BumpCombat.Enemies; using UnityEngine; namespace BumpCombat.Player { [DisallowMultipleComponent] public sealed class PlayerStats : CharacterModel { private const string PressureStreakSourceId = "player.pressure-streak"; private readonly HashSet pressureStreakProcessedEnemyLifetimes = new(); private RunManager subscribedRunManager; private PlayerHealth playerHealth; private float pressureStreakExpiresAt = -1f; private float pressureStreakLastKillAt = -1f; public override float MoveSpeed => Evaluate( CharacterStat.MoveSpeed, GameplayConstants.Current.Player.BaseMoveSpeed); public override float MaxHealth => Evaluate( CharacterStat.MaxHealth, GameplayConstants.Current.Player.BaseMaxHealth); public int PressureStreakKillCount { get; private set; } public int PressureStreakKillTarget => Mathf.Max( 1, GameplayConstants.Current.Player.PressureStreakKillTarget); public float PressureStreakRemaining => Mathf.Max( 0f, pressureStreakExpiresAt - GetCombatTime()); public bool IsPressureStreakWindowActive => PressureStreakRemaining > 0f; public bool IsPressureStreakActive => IsPressureStreakWindowActive && GetStackCount( PressureStreakSourceId, CharacterStat.MoveSpeed) > 0; private void OnEnable() { playerHealth = GetComponent(); if (playerHealth != null) { playerHealth.OnDied += HandlePlayerDied; } CombatEvents.OnValidHit += HandleValidHit; RefreshRunManagerSubscription(); } private void Start() { RefreshRunManagerSubscription(); if (subscribedRunManager != null && !subscribedRunManager.IsTitleScreen && !subscribedRunManager.IsGameOver) { HandleRunStarted(); } } private void Update() { RefreshRunManagerSubscription(); RefreshPressureStreak(); } private void OnDisable() { CombatEvents.OnValidHit -= HandleValidHit; if (playerHealth != null) { playerHealth.OnDied -= HandlePlayerDied; } UnsubscribeRunManager(); ResetPressureStreak(); } private void OnDestroy() { CombatEvents.OnValidHit -= HandleValidHit; if (playerHealth != null) { playerHealth.OnDied -= HandlePlayerDied; } UnsubscribeRunManager(); } public void RefreshPressureStreak() { float now = GetCombatTime(); PrunePressureStreak(now); if (pressureStreakExpiresAt > 0f && now >= pressureStreakExpiresAt) { ClearPressureStreakBuff(); } } private void HandleValidHit(CombatHitResult result) { if (result.Attacker != gameObject || !result.IsOrdinaryBump || result.IsArtifactHit || result.Damage <= 0f || result.Target == null) { return; } if ((playerHealth != null && playerHealth.CurrentHealth <= 0f) || RunManager.Instance?.IsGameOver == true) { return; } EnemyController enemy = result.Target.GetComponent() ?? result.Target.GetComponentInParent(); if (enemy == null || !enemy.IsDead) { return; } int lifetimeIdentity = enemy.SpawnLifetimeIdentity; if (lifetimeIdentity <= 0) { lifetimeIdentity = enemy.GetInstanceID(); } if (!pressureStreakProcessedEnemyLifetimes.Add(lifetimeIdentity)) { return; } float now = GetCombatTime(); RefreshPressureStreak(); if (pressureStreakLastKillAt < 0f || now - pressureStreakLastKillAt > Mathf.Max( 0f, GameplayConstants.Current.Player.PressureStreakWindowSeconds)) { PressureStreakKillCount = 0; } PressureStreakKillCount++; pressureStreakLastKillAt = now; pressureStreakExpiresAt = now + Mathf.Max( 0f, GameplayConstants.Current.Player.PressureStreakDuration); if (PressureStreakKillCount >= PressureStreakKillTarget) { EnsurePressureStreakBuff(); } else { RemoveModifiersFromSource(PressureStreakSourceId); } } private void PrunePressureStreak(float now) { if (pressureStreakLastKillAt >= 0f && now - pressureStreakLastKillAt > Mathf.Max( 0f, GameplayConstants.Current.Player.PressureStreakWindowSeconds)) { PressureStreakKillCount = 0; pressureStreakLastKillAt = -1f; ClearPressureStreakBuff(); } } private void EnsurePressureStreakBuff() { if (GetStackCount( PressureStreakSourceId, CharacterStat.MoveSpeed) > 0) { return; } AddModifier(new StatModifier( PressureStreakSourceId, CharacterStat.MoveSpeed, ModifierOperation.Increased, Mathf.Max( 0f, GameplayConstants.Current.Player.PressureStreakMoveSpeedIncrease))); } private void ClearPressureStreakBuff() { RemoveModifiersFromSource(PressureStreakSourceId); pressureStreakExpiresAt = -1f; } private void ResetPressureStreak() { pressureStreakProcessedEnemyLifetimes.Clear(); PressureStreakKillCount = 0; pressureStreakLastKillAt = -1f; ClearPressureStreakBuff(); } private void HandlePlayerDied() { ResetPressureStreak(); } private void HandleRunStarted() { ResetPressureStreak(); } private void HandleGameOver() { ResetPressureStreak(); } private float GetCombatTime() { RunManager current = RunManager.Instance; return current != null ? current.ElapsedTime : Time.time; } private void RefreshRunManagerSubscription() { RunManager current = RunManager.Instance; if (subscribedRunManager == current) { return; } UnsubscribeRunManager(); subscribedRunManager = current; if (subscribedRunManager == null) { return; } subscribedRunManager.OnRunStarted += HandleRunStarted; subscribedRunManager.OnGameOver += HandleGameOver; } private void UnsubscribeRunManager() { if (subscribedRunManager == null) { return; } subscribedRunManager.OnRunStarted -= HandleRunStarted; subscribedRunManager.OnGameOver -= HandleGameOver; subscribedRunManager = null; } } }