Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
using System;
|
||||
using BumpCombat.Combat;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Constants;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace BumpCombat.Player
|
||||
{
|
||||
[RequireComponent(typeof(PlayerController), typeof(Animator))]
|
||||
[RequireComponent(typeof(PlayerStats))]
|
||||
public sealed class PlayerHealth : MonoBehaviour
|
||||
{
|
||||
private static readonly int HurtParameter = Animator.StringToHash("Hurt");
|
||||
|
||||
// Nonserialized overrides retain isolated test control; normal gameplay
|
||||
// reads its values from the centralized PlayerConstants asset.
|
||||
private float invulnerabilityDuration = -1f;
|
||||
private float knockbackDuration = -1f;
|
||||
private float hurtMovementLockDuration = -1f;
|
||||
private float guardCooldownDuration = -1f;
|
||||
private float guardDuration = -1f;
|
||||
private float guardSuccessCooldownRechargePercent = -1f;
|
||||
|
||||
private PlayerStats playerStats;
|
||||
private PlayerController playerController;
|
||||
private Animator animator;
|
||||
private DashController dashController;
|
||||
private float invulnerableUntil;
|
||||
private float hurtMovementLockedUntil;
|
||||
private float guardUntil;
|
||||
private float guardCooldownUntil;
|
||||
private int nextGuardActivationIdentity;
|
||||
private int activeGuardActivationIdentity;
|
||||
private bool activeGuardHasBlockedAttack;
|
||||
private RunManager subscribedRunManager;
|
||||
|
||||
public event Action<float, float> OnHealthChanged;
|
||||
public event Action<Vector2, float> OnDamaged;
|
||||
/// <summary>Raised only for positive health actually lost after mitigation and lethal clamping.</summary>
|
||||
public event Action<float> OnDamageTaken;
|
||||
public event Action<int> OnGuardAttackBlocked;
|
||||
public event Action<Vector2> OnGuardImpact;
|
||||
public event Action OnDied;
|
||||
|
||||
public float MaxHealth => playerStats.MaxHealth;
|
||||
public float CurrentHealth { get; private set; }
|
||||
public static float DefaultGuardCooldown =>
|
||||
GameplayConstants.Current.Player.GuardCooldownDuration;
|
||||
public static float DefaultGuardDuration =>
|
||||
GameplayConstants.Current.Player.GuardDuration;
|
||||
public static float DefaultGuardSuccessCooldownRechargePercent =>
|
||||
GameplayConstants.Current.Player.GuardSuccessCooldownRechargePercent;
|
||||
public float InvulnerabilityDuration => Mathf.Max(
|
||||
ResolveOverride(invulnerabilityDuration, GameplayConstants.Current.Player.InvulnerabilityDuration),
|
||||
ResolveOverride(hurtMovementLockDuration, GameplayConstants.Current.Player.HurtMovementLockDuration)
|
||||
+ GameplayConstants.Current.Player.HurtRecoveryPadding);
|
||||
public bool IsHurtMovementLocked => Time.time < hurtMovementLockedUntil;
|
||||
public float GuardCooldownDuration => ResolveOverride(
|
||||
guardCooldownDuration,
|
||||
GameplayConstants.Current.Player.GuardCooldownDuration);
|
||||
public float GuardDuration => ResolveOverride(
|
||||
guardDuration,
|
||||
GameplayConstants.Current.Player.GuardDuration);
|
||||
public float GuardSuccessCooldownRechargePercent =>
|
||||
ResolveOverride(
|
||||
guardSuccessCooldownRechargePercent,
|
||||
GameplayConstants.Current.Player.GuardSuccessCooldownRechargePercent);
|
||||
public bool IsGuardAvailable => RunManager.Instance?.IsGuardUnlocked ?? true;
|
||||
public bool IsGuarding =>
|
||||
IsGuardAvailable
|
||||
&& isActiveAndEnabled
|
||||
&& CurrentHealth > 0f
|
||||
&& Time.time < guardUntil;
|
||||
public int ActiveGuardActivationIdentity =>
|
||||
IsGuarding ? activeGuardActivationIdentity : 0;
|
||||
public float GuardCooldownRemaining =>
|
||||
Mathf.Max(0f, guardCooldownUntil - Time.time);
|
||||
/// <summary>Remaining cooldown as a fraction: 1 immediately after use, 0 when ready.</summary>
|
||||
public float GuardCooldownNormalized => GuardCooldownDuration > 0f
|
||||
? Mathf.Clamp01(GuardCooldownRemaining / GuardCooldownDuration)
|
||||
: 0f;
|
||||
/// <summary>Guard readiness as a fraction: 0 during cooldown, 1 when ready.</summary>
|
||||
public float GuardReadinessNormalized => IsGuardAvailable
|
||||
? 1f - GuardCooldownNormalized
|
||||
: 0f;
|
||||
public bool IsInvulnerable =>
|
||||
IsGuarding
|
||||
|| Time.time < invulnerableUntil
|
||||
|| (dashController != null && dashController.IsDashing);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
playerStats = GetComponent<PlayerStats>();
|
||||
playerController = GetComponent<PlayerController>();
|
||||
animator = GetComponent<Animator>();
|
||||
dashController = GetComponent<DashController>();
|
||||
CurrentHealth = MaxHealth;
|
||||
playerStats.OnStatChanged += HandleStatChanged;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||
RefreshRunManagerSubscription();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
InputSystem.onAfterUpdate += HandleInputSystemUpdate;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (playerStats != null)
|
||||
{
|
||||
playerStats.OnStatChanged -= HandleStatChanged;
|
||||
}
|
||||
|
||||
UnsubscribeRunManager();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
InputSystem.onAfterUpdate -= HandleInputSystemUpdate;
|
||||
// A disabled player cannot remain visibly guarded when re-enabled.
|
||||
// Keep the cooldown so disabling cannot refresh a spent guard.
|
||||
guardUntil = 0f;
|
||||
UnsubscribeRunManager();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
RefreshRunManagerSubscription();
|
||||
}
|
||||
|
||||
private void HandleInputSystemUpdate()
|
||||
{
|
||||
InputUpdateType currentUpdateType = InputState.currentUpdateType;
|
||||
if ((currentUpdateType == InputUpdateType.Dynamic
|
||||
|| currentUpdateType == InputUpdateType.Fixed
|
||||
|| currentUpdateType == InputUpdateType.Manual)
|
||||
&& Keyboard.current?.dKey.wasPressedThisFrame == true)
|
||||
{
|
||||
TryActivateGuard();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryActivateGuard()
|
||||
{
|
||||
if (!isActiveAndEnabled
|
||||
|| Time.timeScale <= 0f
|
||||
|| CurrentHealth <= 0f
|
||||
|| !IsGuardAvailable
|
||||
|| !RunManager.GameplayInputEnabled
|
||||
|| IsHurtMovementLocked
|
||||
|| IsGuarding
|
||||
|| GuardCooldownRemaining > 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float now = Time.time;
|
||||
nextGuardActivationIdentity = nextGuardActivationIdentity == int.MaxValue
|
||||
? 1
|
||||
: nextGuardActivationIdentity + 1;
|
||||
activeGuardActivationIdentity = nextGuardActivationIdentity;
|
||||
activeGuardHasBlockedAttack = false;
|
||||
guardUntil = now + GuardDuration;
|
||||
guardCooldownUntil = now + GuardCooldownDuration;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTakeDamage(float damage, Vector2 knockbackDirection, float knockbackDistance)
|
||||
{
|
||||
if (CurrentHealth <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float damageTaken = Mathf.Max(
|
||||
0f,
|
||||
DamageCalculator.FinalizeDamage(
|
||||
playerStats.CalculateIncomingDamage(damage)));
|
||||
if (damageTaken <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsGuarding)
|
||||
{
|
||||
if (!activeGuardHasBlockedAttack)
|
||||
{
|
||||
activeGuardHasBlockedAttack = true;
|
||||
float cooldownRecharge = GuardCooldownDuration
|
||||
* Mathf.Clamp01(
|
||||
GuardSuccessCooldownRechargePercent / 100f);
|
||||
guardCooldownUntil = Mathf.Max(
|
||||
Time.time,
|
||||
guardCooldownUntil - cooldownRecharge);
|
||||
}
|
||||
|
||||
OnGuardAttackBlocked?.Invoke(activeGuardActivationIdentity);
|
||||
OnGuardImpact?.Invoke(knockbackDirection.normalized);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsInvulnerable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float healthBefore = CurrentHealth;
|
||||
|
||||
CurrentHealth = Mathf.Max(0f, CurrentHealth - damageTaken);
|
||||
float actualDamageTaken = healthBefore - CurrentHealth;
|
||||
invulnerableUntil = Time.time + InvulnerabilityDuration;
|
||||
hurtMovementLockedUntil = Time.time + ResolveOverride(
|
||||
hurtMovementLockDuration,
|
||||
GameplayConstants.Current.Player.HurtMovementLockDuration);
|
||||
playerController.ApplyDamageKnockback(
|
||||
knockbackDirection,
|
||||
knockbackDistance,
|
||||
ResolveOverride(
|
||||
knockbackDuration,
|
||||
GameplayConstants.Current.Player.KnockbackDuration));
|
||||
animator.SetTrigger(HurtParameter);
|
||||
OnDamaged?.Invoke(knockbackDirection.normalized, knockbackDistance);
|
||||
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||
if (actualDamageTaken > 0f)
|
||||
{
|
||||
OnDamageTaken?.Invoke(actualDamageTaken);
|
||||
}
|
||||
|
||||
if (CurrentHealth <= 0f)
|
||||
{
|
||||
OnDied?.Invoke();
|
||||
RunManager.Instance?.EndRun();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static float ResolveOverride(float value, float configuredValue)
|
||||
{
|
||||
return value >= 0f ? value : configuredValue;
|
||||
}
|
||||
|
||||
public void Heal(float amount)
|
||||
{
|
||||
if (amount <= 0f || CurrentHealth <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentHealth = Mathf.Min(MaxHealth, CurrentHealth + amount);
|
||||
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||
}
|
||||
|
||||
public void GrantInvulnerability(float duration)
|
||||
{
|
||||
if (duration <= 0f || CurrentHealth <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
invulnerableUntil = Mathf.Max(
|
||||
invulnerableUntil,
|
||||
Time.time + duration);
|
||||
}
|
||||
|
||||
private void HandleStatChanged(CharacterStat stat)
|
||||
{
|
||||
if (stat != CharacterStat.MaxHealth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentHealth = Mathf.Min(CurrentHealth, MaxHealth);
|
||||
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||
}
|
||||
|
||||
private void HandleRunStarted()
|
||||
{
|
||||
guardUntil = 0f;
|
||||
guardCooldownUntil = 0f;
|
||||
nextGuardActivationIdentity = 0;
|
||||
activeGuardActivationIdentity = 0;
|
||||
activeGuardHasBlockedAttack = false;
|
||||
}
|
||||
|
||||
private void RefreshRunManagerSubscription()
|
||||
{
|
||||
RunManager current = RunManager.Instance;
|
||||
if (subscribedRunManager == current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnsubscribeRunManager();
|
||||
subscribedRunManager = current;
|
||||
if (subscribedRunManager != null)
|
||||
{
|
||||
subscribedRunManager.OnRunStarted += HandleRunStarted;
|
||||
subscribedRunManager.OnGuardAvailabilityChanged +=
|
||||
HandleGuardAvailabilityChanged;
|
||||
HandleGuardAvailabilityChanged(
|
||||
subscribedRunManager.IsGuardUnlocked);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGuardAvailabilityChanged(bool isAvailable)
|
||||
{
|
||||
if (!isAvailable)
|
||||
{
|
||||
guardUntil = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeRunManager()
|
||||
{
|
||||
if (subscribedRunManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
subscribedRunManager.OnRunStarted -= HandleRunStarted;
|
||||
subscribedRunManager.OnGuardAvailabilityChanged -=
|
||||
HandleGuardAvailabilityChanged;
|
||||
subscribedRunManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user