320 lines
7.1 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|