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

493 lines
13 KiB
C#

using UnityEngine;
using Fusion;
using System.Collections.Generic;
namespace TPSBR
{
public abstract class FirearmWeapon : Weapon, IDynamicPickupProvider
{
// PUBLIC MEMBERS
public bool IsFiring { get { return _state.IsBitSet(0); } set { _state = _state.SetBitNoRef(0, value); } }
public bool IsReloading { get { return _state.IsBitSet(1); } set { _state = _state.SetBitNoRef(1, value); } }
public int MagazineAmmo => _magazineAmmo;
public int MaxMagazineAmmo => _maxMagazineAmmo;
public int WeaponAmmo => _weaponAmmo;
public int TotalAmmo => _magazineAmmo + _weaponAmmo;
public int InitialAmmo => _initialAmmo;
public float ReloadTime => _reloadTime;
public Transform FireTransform => _fireTransform;
public float Cooldown => _cooldown.ExpiredOrNotRunning(Runner) == false ? _cooldown.RemainingTime(Runner).Value : 0f;
public float TotalDispersion => GetTotalDispersion();
[Networked, HideInInspector]
public Vector2 Recoil { get; set; }
// PRIVATE MEMBERS
[Header("General")]
[SerializeField]
private int _cadence = 600;
[SerializeField]
protected int _initialAmmo = 150;
[SerializeField]
private int _maxMagazineAmmo = 30;
[SerializeField]
private int _maxWeaponAmmo = 120;
[SerializeField]
private bool _hasUnlimitedAmmo;
[SerializeField]
private float _reloadTime = 2f;
[SerializeField]
private bool _fireOnKeyDownOnly;
[SerializeField]
private int _projectilesPerShot = 1;
[SerializeField]
private ShakeSetup _cameraShakePosition;
[SerializeField]
private ShakeSetup _cameraShakeRotation;
[SerializeField]
private bool _supressReloadWhileAimed;
[Header("Dispersion")]
[SerializeField]
private float _minDispersion = 0.3f;
[SerializeField]
private float _minFireDispersion = 1f;
[SerializeField]
private float _maxFireDispersion = 7f;
[SerializeField]
private float _maxDispersion = 15f;
[SerializeField]
private float _dispersionIncreasePerShot = 0.5f;
[SerializeField]
private float _dispersionDecreaseRate = 10f;
[Header("Recoil")]
[SerializeField]
private RecoilPattern _recoilPattern;
[SerializeField]
private float _recoilAimMultiplier = 1f;
[SerializeField]
private float _recoilSpeedMultiplier = 1f;
[SerializeField]
private float _recoilDecreaseSpeed = 20f;
[Header("Audio")]
[SerializeField]
private Transform _audioEffectsTransform;
[SerializeField]
private AudioEffect _localAudio;
[SerializeField]
private AudioSetup _fireSound;
[SerializeField]
private AudioSetup _reloadSound;
[SerializeField]
private AudioSetup _readySound;
[Header("Fire")]
[SerializeField]
private Transform _fireTransform;
[SerializeField]
private GameObject _fireParticlePlayer;
[SerializeField]
private GameObject _fireParticleProxy;
[Networked]
protected byte _state { get; set; }
[Networked][OnChangedRender(nameof(OnCooldownChanged))]
protected TickTimer _cooldown { get; set; }
[Networked, HideInInspector]
protected int _projectilesCount { get; set; }
[Networked]
protected int _magazineAmmo { get; private set; }
[Networked]
protected int _weaponAmmo { get; private set; }
[Networked]
protected float _dispersion { get; private set; }
[Networked]
protected int _recoilStartShot { get; private set; }
private int _lastVisibleProjectileCount;
private bool _proxyWasOutsideOfAOI;
private int _fireTicks;
private int _recoilTicks;
private List<LagCompensatedHit> _validHits = new List<LagCompensatedHit>(16);
// Weapon INTERFACE
public override void Spawned()
{
base.Spawned();
_magazineAmmo = Mathf.Clamp(_initialAmmo, 0, _maxMagazineAmmo);
_weaponAmmo = Mathf.Clamp(_initialAmmo - _magazineAmmo, 0, _maxWeaponAmmo);
_dispersion = _minFireDispersion;
float fireTime = 60f / _cadence;
_fireTicks = (int)System.Math.Ceiling(fireTime / (double)Runner.DeltaTime);
_recoilTicks = Mathf.Min(_fireTicks, (int)(_fireTicks / _recoilSpeedMultiplier));
_lastVisibleProjectileCount = _projectilesCount;
_proxyWasOutsideOfAOI = false;
}
public override void FixedUpdateNetwork()
{
base.FixedUpdateNetwork();
if (IsProxy == true)
{
if (Object.LastReceiveTick < Runner.Tick - 2.0f * Runner.Config.Simulation.TickRateSelection.Client)
{
_proxyWasOutsideOfAOI = true;
}
return;
}
bool recoilInProgress = IsFiring == true && _recoilPattern != null && _cooldown.RemainingTicks(Runner) >= (_fireTicks - _recoilTicks);
if (recoilInProgress == true)
{
float aimMultiplier = Character != null && Character.CharacterController.Data.Aim == true ? _recoilAimMultiplier : 1.0f;
Vector2 shotRecoil = _recoilPattern.GetRecoil(_projectilesCount - _recoilStartShot) * aimMultiplier;
Recoil += shotRecoil / _recoilTicks;
}
else if (Recoil != Vector2.zero)
{
Recoil = Vector2.Lerp(Recoil, Vector2.zero, Runner.DeltaTime * _recoilDecreaseSpeed);
float recoilSqrMagnitude = Recoil.sqrMagnitude;
if (recoilSqrMagnitude < 0.05f)
{
_recoilStartShot = _projectilesCount + 1;
}
if (recoilSqrMagnitude < 0.005f)
{
Recoil = Vector2.zero;
}
}
if (IsFiring == false)
{
_dispersion = Mathf.Clamp(_dispersion - Runner.DeltaTime * _dispersionDecreaseRate, _minFireDispersion, _maxDispersion);
}
int? cooldownTargetTick = _cooldown.TargetTick;
if (cooldownTargetTick.HasValue == true && cooldownTargetTick.Value <= Runner.Tick)
{
if (IsReloading == true)
{
int reloadAmmo = _maxMagazineAmmo - _magazineAmmo;
if (_hasUnlimitedAmmo == false)
{
reloadAmmo = Mathf.Min(reloadAmmo, _weaponAmmo);
_weaponAmmo -= reloadAmmo;
}
_magazineAmmo += reloadAmmo;
}
IsFiring = false;
IsReloading = false;
}
}
public override void Render()
{
base.Render();
if (Runner.Mode != SimulationModes.Server)
{
UpdateVisibleProjectiles();
}
}
public override bool IsBusy()
{
return IsFiring || IsReloading;
}
public override bool CanFire(bool keyDown)
{
if (IsFiring == true || IsReloading == true)
return false;
if (_magazineAmmo <= 0)
return false;
if (_fireOnKeyDownOnly == true && keyDown == false)
return false;
return _cooldown.ExpiredOrNotRunning(Runner);
}
public override bool CanReload(bool autoReload)
{
if (IsFiring == true || IsReloading == true)
return false;
if (_magazineAmmo >= _maxMagazineAmmo)
return false;
if (_weaponAmmo <= 0)
return false;
if (_cooldown.ExpiredOrNotRunning(Runner) == false)
return false;
if (_supressReloadWhileAimed == true && Character != null && Character.CharacterController.Data.Aim == true)
return false;
return autoReload == false || _magazineAmmo <= 0;
}
public override bool CanAim()
{
return IsReloading == false;
}
public override void Fire(Vector3 firePosition, Vector3 targetPosition, LayerMask hitMask)
{
if (CanFire(true) == false)
return;
IsFiring = true;
_magazineAmmo--;
Vector3 direction = targetPosition - firePosition;
float distanceToTarget = direction.magnitude;
direction /= distanceToTarget;
if (_dispersion > 0f)
{
Random.InitState((_projectilesCount + 10) * Runner.Tick);
}
for (int i = 0; i < _projectilesPerShot; i++)
{
var projectileDirection = direction;
if (_dispersion > 0f)
{
var randomDispersion = Random.insideUnitSphere * TotalDispersion;
projectileDirection = Quaternion.Euler(randomDispersion.x, randomDispersion.y, randomDispersion.z) * direction;
}
if (FireProjectile(firePosition, targetPosition, projectileDirection, distanceToTarget, hitMask, i == 0) == true)
{
_projectilesCount++;
}
}
if (_dispersion > 0f && _dispersion < _maxFireDispersion)
{
_dispersion = Mathf.Clamp(_dispersion + _dispersionIncreasePerShot, _minFireDispersion, _maxFireDispersion);
}
_cooldown = TickTimer.CreateFromTicks(Runner, _fireTicks);
}
public override void Reload()
{
if (CanReload(false) == false)
return;
IsReloading = true;
_cooldown = TickTimer.CreateFromSeconds(Runner, _reloadTime);
_dispersion = _minFireDispersion;
}
public override void AssignFireAudioEffects(Transform root, AudioEffect[] audioEffects)
{
if (root != null)
{
root.SetParent(_audioEffectsTransform, false);
}
base.AssignFireAudioEffects(root, audioEffects);
}
public override bool HasAmmo()
{
return _magazineAmmo > 0 || _weaponAmmo > 0;
}
public override bool AddAmmo(int ammo)
{
if (_weaponAmmo >= _maxWeaponAmmo)
return false;
_weaponAmmo = Mathf.Clamp(_weaponAmmo + ammo, 0, _maxWeaponAmmo);
return true;
}
public override bool CanFireToPosition(Vector3 firePosition, ref Vector3 targetPosition, LayerMask hitMask)
{
Vector3 direction = targetPosition - firePosition;
float distanceToTarget = direction.magnitude;
direction /= distanceToTarget;
bool positionReached = true;
int ownerObjectID = Owner != null ? Owner.gameObject.GetInstanceID() : 0;
if (ProjectileUtility.ProjectileCast(Runner, Object.InputAuthority, ownerObjectID, firePosition, direction, distanceToTarget, distanceToTarget + 1, hitMask, _validHits) == true)
{
var hit = _validHits[0];
if (hit.GameObject != null && hit.GameObject.layer == ObjectLayer.Agent)
{
// Do not show "position not reached" indicator if we accidentally hit Agent,
// this is what we want
positionReached = true;
targetPosition = hit.Point;
}
else
{
positionReached = (hit.Point - targetPosition).sqrMagnitude < 0.5f;
targetPosition = hit.Point;
}
}
return positionReached;
}
protected override void OnWeaponArmed()
{
if (Runner.IsForward == true)
{
PlayLocalSound(_readySound);
}
}
protected override void OnWeaponDisarmed()
{
if (Runner.IsForward == true)
{
IsReloading = false;
}
}
// IPickupProvider INTERFACE
string IDynamicPickupProvider.Description => GetPickupDescription();
float IDynamicPickupProvider.DespawnTime => WeaponAmmo == 0 && MagazineAmmo == 0 ? 5f : 60f;
// FireamWeapon INTERFACE
protected virtual bool FireProjectile(Vector3 firePosition, Vector3 targetPosition, Vector3 direction, float distanceToTarget, LayerMask hitMask, bool isFirst)
{
return false;
}
protected virtual void FireVisualProjectile(int projectileIndex, bool playFireEffects)
{
if (playFireEffects == true)
{
var particlePrefab = HasInputAuthority == true ? _fireParticlePlayer : _fireParticleProxy;
if (particlePrefab != null)
{
var fireParticle = Context.ObjectCache.Get(particlePrefab);
Context.ObjectCache.ReturnDeferred(fireParticle, HitType == EHitType.Sniper ? 5f : 1f);
Runner.MoveToRunnerSceneExtended(fireParticle);
fireParticle.transform.SetParent(_fireTransform, false);
}
PlaySound(_fireSound);
if (Context.ObservedAgent.Object == Owner)
{
Context.Camera.ShakeEffect.Play(_cameraShakePosition, EShakeForce.ReplaceSame);
Context.Camera.ShakeEffect.Play(_cameraShakeRotation, EShakeForce.ReplaceSame);
}
}
}
// PROTECTED METHODS
protected void OnCooldownChanged()
{
if (IsReloading == true)
{
PlayLocalSound(_reloadSound);
}
}
protected void PlayLocalSound(AudioSetup sound)
{
if (ApplicationSettings.IsStrippedBatch == true)
return;
if (_localAudio.CurrentSetup == sound && _localAudio.IsPlaying == true)
return;
_localAudio.Play(sound, EForceBehaviour.ForceAny);
}
// PRIVATE METHODS
private void UpdateVisibleProjectiles()
{
if (_lastVisibleProjectileCount == _projectilesCount)
return;
if (_proxyWasOutsideOfAOI == true && Object.LastReceiveTick >= Runner.Tick - Runner.Config.Simulation.TickRateSelection.Client)
{
_proxyWasOutsideOfAOI = false;
if (_cooldown.ExpiredOrNotRunning(Runner) == true)
{
// Too far behind, do not spawn any fire visuals
_lastVisibleProjectileCount = _projectilesCount;
return;
}
else
{
// The player just fired and was outside of AoI, we should spawn at least one effect when getting in
_lastVisibleProjectileCount = _projectilesCount - 1;
}
}
int missingShots = _projectilesCount - _lastVisibleProjectileCount;
for (int i = 0; i < missingShots; i++)
{
FireVisualProjectile(_lastVisibleProjectileCount + i, i == 0);
}
_lastVisibleProjectileCount = _projectilesCount;
}
private float GetTotalDispersion()
{
float multiplier = Character != null ? Character.DispersionMultiplier : 1f;
return Mathf.Clamp(_dispersion * multiplier, _minDispersion, _maxDispersion);
}
private string GetPickupDescription()
{
// For prefab read initial data
if (gameObject.scene.rootCount == 0)
{
int magazineAmmo = Mathf.Clamp(_initialAmmo, 0, _maxMagazineAmmo);
int weaponAmmo = Mathf.Clamp(_initialAmmo - magazineAmmo, 0, _maxWeaponAmmo);
return $"Ammo {magazineAmmo} / {weaponAmmo}";
}
return $"Ammo {MagazineAmmo} / {WeaponAmmo}";
}
}
}