2025-09-24 11:24:38 +05:00

320 lines
7.1 KiB
C#

using System;
using Fusion;
namespace TPSBR
{
using UnityEngine;
public struct BodyHitData : INetworkStruct
{
public EHitAction Action;
public float Damage;
public Vector3 RelativePosition;
public Vector3 Direction;
public PlayerRef Instigator;
}
public sealed class Health : ContextBehaviour, IHitTarget, IHitInstigator
{
// PUBLIC MEMBERS
public bool IsAlive => CurrentHealth > 0f;
public float MaxHealth => _maxHealth;
public float MaxShield => _maxShield;
[Networked, HideInInspector]
public float CurrentHealth { get; private set; }
[Networked, HideInInspector]
public float CurrentShield { get; private set; }
public event Action<HitData> HitTaken;
public event Action<HitData> HitPerformed;
// PRIVATE MEMBERS
[SerializeField]
private float _maxHealth;
[SerializeField]
private float _maxShield;
[SerializeField]
private float _startShield;
[SerializeField]
private Transform _hitIndicatorPivot;
[Header("Regeneration")]
[SerializeField]
private float _healthRegenPerSecond;
[SerializeField]
private float _maxHealthFromRegen;
[SerializeField]
private int _regenTickPerSecond;
[SerializeField]
private int _regenCombatDelay;
[Networked]
private int _hitCount { get; set; }
[Networked, Capacity(4)]
private NetworkArray<BodyHitData> _hitData { get; }
private int _visibleHitCount;
private Agent _agent;
private TickTimer _regenTickTimer;
private float _healthRegenPerTick;
private float _regenTickTime;
// PUBLIC METHODS
public void OnSpawned(Agent agent)
{
_visibleHitCount = _hitCount;
}
public void OnDespawned()
{
HitTaken = null;
HitPerformed = null;
}
public void OnFixedUpdate()
{
if (HasStateAuthority == false)
return;
if (IsAlive == true && _healthRegenPerSecond > 0f && _regenTickTimer.ExpiredOrNotRunning(Runner) == true)
{
_regenTickTimer = TickTimer.CreateFromSeconds(Runner, _regenTickTime);
var healthDiff = _maxHealthFromRegen - CurrentHealth;
if (healthDiff <= 0f)
return;
AddHealth(Mathf.Min(healthDiff, _healthRegenPerTick));
}
}
public void ResetRegenDelay()
{
_regenTickTimer = TickTimer.CreateFromSeconds(Runner, _regenCombatDelay);
}
public override void CopyBackingFieldsToState(bool firstTime)
{
base.CopyBackingFieldsToState(firstTime);
InvokeWeavedCode();
CurrentHealth = _maxHealth;
CurrentShield = _startShield;
}
// NetworkBehaviour INTERFACE
public override void Render()
{
if (Runner.Mode != SimulationModes.Server)
{
UpdateVisibleHits();
}
}
// MONOBEHAVIOUR
private void Awake()
{
_agent = GetComponent<Agent>();
_regenTickTime = 1f / _regenTickPerSecond;
_healthRegenPerTick = _healthRegenPerSecond / _regenTickPerSecond;
}
// IHitTarget INTERFACE
Transform IHitTarget.HitPivot => _hitIndicatorPivot != null ? _hitIndicatorPivot : transform;
void IHitTarget.ProcessHit(ref HitData hitData)
{
if (IsAlive == false)
{
hitData.Amount = 0;
return;
}
ApplyHit(ref hitData);
if (IsAlive == false)
{
hitData.IsFatal = true;
Context.GameplayMode.AgentDeath(_agent, hitData);
}
}
// IHitInstigator INTERFACE
void IHitInstigator.HitPerformed(HitData hitData)
{
if (hitData.Amount > 0 && hitData.Target != (IHitTarget)this && Runner.IsResimulation == false)
{
HitPerformed?.Invoke(hitData);
}
}
// PRIVATE METHODS
private void ApplyHit(ref HitData hit)
{
if (IsAlive == false)
return;
if (hit.Action == EHitAction.Damage)
{
hit.Amount = ApplyDamage(hit.Amount);
}
else if (hit.Action == EHitAction.Heal)
{
hit.Amount = AddHealth(hit.Amount);
}
else if (hit.Action == EHitAction.Shield)
{
hit.Amount = AddShield(hit.Amount);
}
if (hit.Amount <= 0)
return;
// Hit taken effects (blood) is shown immediately for local player, for other
// effects (hit number, crosshair hit effect) we are waiting for server confirmation
if (hit.InstigatorRef == Context.LocalPlayerRef && Runner.IsForward == true)
{
HitTaken?.Invoke(hit);
}
if (HasStateAuthority == false)
return;
_hitCount++;
var bodyHitData = new BodyHitData
{
Action = hit.Action,
Damage = hit.Amount,
Direction = hit.Direction,
RelativePosition = hit.Position != Vector3.zero ? hit.Position - transform.position : Vector3.zero,
Instigator = hit.InstigatorRef,
};
int hitIndex = _hitCount % _hitData.Length;
_hitData.Set(hitIndex, bodyHitData);
}
private float ApplyDamage(float damage)
{
if (damage <= 0f)
return 0f;
ResetRegenDelay();
var shieldChange = AddShield(-damage);
var healthChange = AddHealth(-(damage + shieldChange));
return -(shieldChange + healthChange);
}
private float AddHealth(float health)
{
float previousHealth = CurrentHealth;
SetHealth(CurrentHealth + health);
return CurrentHealth - previousHealth;
}
private float AddShield(float shield)
{
float previousShield = CurrentShield;
SetShield(CurrentShield + shield);
return CurrentShield - previousShield;
}
private void SetHealth(float health)
{
CurrentHealth = Mathf.Clamp(health, 0, _maxHealth);
}
private void SetShield(float shield)
{
CurrentShield = Mathf.Clamp(shield, 0, _maxShield);
}
private void UpdateVisibleHits()
{
if (_visibleHitCount == _hitCount)
return;
int dataCount = _hitData.Length;
int oldestHitData = _hitCount - dataCount + 1;
for (int i = Mathf.Max(_visibleHitCount + 1, oldestHitData); i <= _hitCount; i++)
{
int shotIndex = i % dataCount;
var bodyHitData = _hitData.Get(shotIndex);
var hitData = new HitData
{
Action = bodyHitData.Action,
Amount = bodyHitData.Damage,
Position = transform.position + bodyHitData.RelativePosition,
Direction = bodyHitData.Direction,
Normal = -bodyHitData.Direction,
Target = this,
InstigatorRef = bodyHitData.Instigator,
IsFatal = i == _hitCount && CurrentHealth <= 0f,
};
OnHitTaken(hitData);
}
_visibleHitCount = _hitCount;
}
private void OnHitTaken(HitData hit)
{
// For local player, HitTaken was already called when applying hit
if (hit.InstigatorRef != Context.LocalPlayerRef)
{
HitTaken?.Invoke(hit);
}
// We use _hitData buffer to inform instigator about successful hit as this needs
// to be synchronized over network as well (e.g. when spectating other players)
if (hit.InstigatorRef.IsRealPlayer == true && hit.InstigatorRef == Context.ObservedPlayerRef)
{
var instigator = hit.Instigator;
if (instigator == null)
{
var player = Context.NetworkGame.GetPlayer(hit.InstigatorRef);
instigator = player != null ? player.ActiveAgent.Health as IHitInstigator : null;
}
if (instigator != null)
{
instigator.HitPerformed(hit);
}
}
}
// DEBUG
[ContextMenu("Add Health")]
private void Debug_AddHealth()
{
CurrentHealth += 10;
}
[ContextMenu("Remove Health")]
private void Debug_RemoveHealth()
{
CurrentHealth -= 10;
}
}
}