Files
2026-03-03 05:27:03 +05:00

595 lines
21 KiB
C#

using System;
using System.Collections;
using System.Reactive.Disposables;
using UnityEngine;
using UHFPS.Input;
using UHFPS.Tools;
using UHFPS.Scriptable;
using Newtonsoft.Json.Linq;
using static UHFPS.Scriptable.SurfaceDefinitionSet;
namespace UHFPS.Runtime
{
public class GunItem : PlayerItemBehaviour
{
public enum WeaponTypeEnum { Semi, Auto }
[Serializable]
public sealed class BaseSettings
{
public int BaseDamage = 20;
public float DropoffDistance = 500f;
public float RangeModifier = 1f;
public float ShootRange = 250.0f;
[Header("Other")]
public float RoundsPerMinute = 600f;
public float Hitforce = 20.0f;
public float HeadModifier = 1f;
[Header("Debug")]
public bool VisualizeDistance;
[Range(3, 10)]
public uint DropoffSections = 4;
}
[Serializable]
public sealed class RecoilSettings
{
public float BaseRecoil = 0.1f;
[Header("Recoil Multipliers")]
public float ADSMultiplier = 0.5f;
public float WalkMultiplier = 2f;
public float RunMultiplier = 3f;
public float CrouchMultiplier = 0.75f;
public float CrouchWalkMultiplier = 0.5f;
public float JumpMultiplier = 5f;
[Header("Settings")]
public float RecoilChangeSpeed = 0.1f;
public float CrouchWalkVel = 0.1f;
public float SteadyVelocity = 0.1f;
}
[Serializable]
public sealed class GunProperties
{
public bool EnableAiming = true;
public bool EnableAdsFov = true;
public bool KeepReloadMagBullets = true;
}
[Serializable]
public sealed class AimingSettings
{
public Vector3 AimPosition;
[Range(0f, 1f)]
public float AimMotionsWeight = 0.5f;
[Header("Weapon Model Aiming")]
public float AimSpeed = 20f;
public float BackAimSpeed = 30f;
[Header("ADS Aiming")]
public float ADSCameraFOV = 40f;
public float ADSAimSpeed = 20f;
public float ADSBackAimSpeed = 30f;
}
[Serializable]
public sealed class AttachmentSettings
{
public GameObject FlashlightAttachment;
public Light FlashlightLight;
public SoundClip SwitchSound;
}
[Serializable]
public sealed class BulletSettings
{
public string AmmoTextFormat = "<size=30><color=#4d828f>{0}</color> / </size>{1}";
public int BulletsPerMag = 0;
public int BulletsPerShot = 1;
}
[Serializable]
public sealed class BulletAndMuzzleFlash
{
public Transform BarrelEnd;
public GameObject MuzzleFlash;
[Header("Bullet Settings")]
public GameObject BulletPrefab;
public Color TrailColor = Color.white;
public float BulletForce;
[Header("Muzzle Flash Settings")]
public Axis MuzzleAxis;
public float MuzzleFlashDelayTime = 0.01f;
public float MuzzleFlashShowTime = 0.01f;
}
[Serializable]
public sealed class AnimationSettings
{
public string GunDrawState = "Draw";
public string GunHideState = "Hide";
public string GunIdleState = "Idle";
public string GunShootState = "Shoot";
public string GunReloadState = "Reload";
[Header("Triggers")]
public string HideTrigger = "Hide";
public string ShootTrigger = "Shoot";
public string ReloadTrigger = "Reload";
}
[Serializable]
public sealed class GunSounds
{
public SoundClip DrawSound;
public SoundClip HideSound;
public SoundClip ShootSound;
}
public string GunName = "Pistol";
public WeaponTypeEnum WeaponType;
public LayerMask RaycastMask;
public SurfaceDefinitionSet SurfaceDefinitionSet;
public SurfaceDetection SurfaceDetection;
public Tag FleshTag;
public ItemGuid GunInventoryItem;
public ItemGuid AmmoInventoryItem;
public ItemGuid FlashlightAttachmentItem;
public BaseSettings baseSettings;
public RecoilSettings recoilSettings;
public GunProperties gunProperties;
public AimingSettings aimingSettings;
public AttachmentSettings attachmentSettings;
public BulletSettings bulletSettings;
public BulletAndMuzzleFlash bulletAndMuzzleFlash;
public AnimationSettings animationSettings;
public GunSounds gunSounds;
[SerializeField] private float newRecoil;
[SerializeField] private int carryingBullets;
[SerializeField] private int bulletsInMag;
private GameManager gameManager;
private Inventory inventory;
private CompositeDisposable disposables = new();
private AudioSource audioSource;
private MeshRenderer muzzleRenderer;
private CanvasGroup ammoPanel;
private TMPro.TMP_Text ammoText;
private Vector3 defaultAim;
private float defaultFov;
private float currentRecoil;
private float currentFov;
private float fireRate;
private float fireTime;
private float muzzleDelayTime;
private float muzzleShowTime;
private bool isEquipped;
private bool isBusy;
private bool isReloading;
private bool flashlightAttached;
private bool flashlightEnabled;
public override string Name => GunName;
public override bool IsBusy() => !isEquipped || isBusy;
private void Awake()
{
gameManager = GameManager.Instance;
inventory = Inventory.Instance;
audioSource = GetComponent<AudioSource>();
muzzleRenderer = bulletAndMuzzleFlash.MuzzleFlash.GetComponentInChildren<MeshRenderer>();
var behaviours = gameManager.GraphicReferences.Value["Gun"];
ammoPanel = (CanvasGroup)behaviours[0];
ammoText = (TMPro.TMP_Text)behaviours[1];
defaultAim = Vector3.zero;
defaultFov = PlayerManager.MainVirtualCamera.m_Lens.FieldOfView;
currentFov = defaultFov;
currentRecoil = recoilSettings.BaseRecoil;
disposables.Add(inventory.OnInventoryChanged.Subscribe(item =>
{
if (item.guid == AmmoInventoryItem)
{
carryingBullets = inventory.GetAllItemsQuantity(AmmoInventoryItem);
UpdateAmmoText();
}
}));
}
public override void OnDestroy()
{
base.OnDestroy();
disposables.Dispose();
}
public override void OnUpdate()
{
if (!isEquipped)
return;
fireRate = 60 / baseSettings.RoundsPerMinute;
if (fireTime > 0) fireTime -= Time.deltaTime;
fireTime = Mathf.Clamp(fireTime, 0, Mathf.Infinity);
if (bulletAndMuzzleFlash.MuzzleFlash != null)
{
if(muzzleDelayTime > 0) muzzleDelayTime -= Time.deltaTime;
else if(muzzleShowTime > 0)
{
bulletAndMuzzleFlash.MuzzleFlash.SetActive(true);
muzzleShowTime -= Time.deltaTime;
}
else
{
bulletAndMuzzleFlash.MuzzleFlash.SetActive(false);
}
muzzleShowTime = Mathf.Clamp(muzzleShowTime, 0, Mathf.Infinity);
muzzleDelayTime = Mathf.Clamp(muzzleDelayTime, 0, Mathf.Infinity);
}
if (CanInteract)
{
if (WeaponType == WeaponTypeEnum.Semi)
{
if (InputManager.ReadButtonOnce("Fire", Controls.FIRE) && !isReloading && fireTime <= 0)
{
FireOneBullet();
fireTime = fireRate;
}
}
else if (WeaponType == WeaponTypeEnum.Auto)
{
if (InputManager.ReadButton(Controls.FIRE) && !isReloading && fireTime <= 0)
{
FireOneBullet();
fireTime = fireRate;
}
}
if (!isReloading)
{
if (carryingBullets > 0 && bulletsInMag != bulletSettings.BulletsPerMag)
{
if (InputManager.ReadButtonOnce(this, Controls.RELOAD))
{
StartCoroutine(ReloadGun());
isReloading = true;
}
}
}
else
{
fireTime = 0f;
}
if (flashlightAttached)
{
if (InputManager.ReadButtonOnce(this, Controls.FLASHLIGHT) && !isReloading)
{
flashlightEnabled = !flashlightEnabled;
attachmentSettings.FlashlightLight.enabled = flashlightEnabled;
audioSource.PlayOneShotSoundClip(attachmentSettings.SwitchSound);
}
}
}
if (gunProperties.EnableAiming)
{
if (CanInteract && !isReloading && InputManager.ReadButton(Controls.ADS))
{
transform.localPosition = Vector3.Lerp(transform.localPosition, aimingSettings.AimPosition, Time.deltaTime * aimingSettings.AimSpeed);
if (gunProperties.EnableAdsFov)
{
currentFov = Mathf.Lerp(currentFov, aimingSettings.ADSCameraFOV, Time.deltaTime * aimingSettings.ADSAimSpeed);
PlayerManager.MainVirtualCamera.m_Lens.FieldOfView = currentFov;
}
MotionBlender.Weight = aimingSettings.AimMotionsWeight;
}
else
{
transform.localPosition = Vector3.Lerp(transform.localPosition, defaultAim, Time.deltaTime * aimingSettings.BackAimSpeed);
if (gunProperties.EnableAdsFov)
{
currentFov = Mathf.Lerp(currentFov, defaultFov, Time.deltaTime * aimingSettings.ADSBackAimSpeed);
PlayerManager.MainVirtualCamera.m_Lens.FieldOfView = currentFov;
}
MotionBlender.Weight = 1f;
}
}
}
private void FireOneBullet()
{
if (bulletsInMag <= 0)
{
bulletsInMag = 0;
return;
}
Animator.SetTrigger(animationSettings.ShootTrigger);
audioSource.PlayOneShotSoundClip(gunSounds.ShootSound);
if (bulletAndMuzzleFlash.MuzzleFlash)
{
bulletAndMuzzleFlash.MuzzleFlash.SetActive(true);
if (muzzleRenderer != null)
{
Vector3 muzzleRot = muzzleRenderer.transform.localEulerAngles;
muzzleRot += 360 * UnityEngine.Random.value * bulletAndMuzzleFlash.MuzzleAxis.Convert();
muzzleRenderer.transform.localEulerAngles = muzzleRot;
}
muzzleDelayTime = bulletAndMuzzleFlash.MuzzleFlashDelayTime;
muzzleShowTime = bulletAndMuzzleFlash.MuzzleFlashShowTime;
}
for (int i = 0; i < bulletSettings.BulletsPerShot; i++)
{
float width = UnityEngine.Random.Range(-1f, 1f) * currentRecoil;
float height = UnityEngine.Random.Range(-1f, 1f) * currentRecoil;
Transform mainCamera = PlayerManager.MainCamera.transform;
Vector3 spray = mainCamera.forward + mainCamera.transform.right * width + mainCamera.up * height;
Ray aim = new(mainCamera.position, spray.normalized);
if (Physics.Raycast(aim, out RaycastHit hit, baseSettings.ShootRange, RaycastMask))
ShowBulletMark(hit, aim.direction);
if (bulletAndMuzzleFlash.BulletPrefab != null)
{
Vector3 direction = hit.collider != null ? hit.point - bulletAndMuzzleFlash.BarrelEnd.position : aim.direction;
direction.Normalize();
if ((direction.z > 0 && aim.direction.z > 0) || (direction.z < 0 && aim.direction.z < 0))
{
ShowWeaponTrace(direction);
}
}
}
bulletsInMag--;
ApplyEffect("Shoot");
inventory.SetItemQuantity(GunInventoryItem, (ushort)bulletsInMag, false);
UpdateAmmoText();
}
private void ShowBulletMark(RaycastHit hit, Vector3 dir)
{
bool isFlesh = false;
if (hit.collider.TryGetComponent(out IDamagable damagable))
{
float baseDamage = baseSettings.BaseDamage;
float rangeModifier = baseSettings.RangeModifier;
float maxDistance = baseSettings.DropoffDistance;
int damage = Mathf.RoundToInt(baseDamage * Mathf.Pow(rangeModifier, hit.distance / maxDistance));
damagable.OnApplyDamage(damage, PlayerManager.transform);
isFlesh = damagable is NPCBodyPart or IHealthEntity;
}
if (hit.rigidbody != null) hit.rigidbody.AddForceAtPosition(dir * baseSettings.Hitforce, hit.point);
Quaternion hitRotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
GameObject hitObject = hit.collider.gameObject;
Vector3 hitPoint = hit.point;
SurfaceDefinition surfaceDefinition = isFlesh
? SurfaceDefinitionSet.GetSurface(FleshTag)
: SurfaceDefinitionSet.GetSurface(hitObject, hitPoint, SurfaceDetection);
if (surfaceDefinition != null)
{
if (surfaceDefinition.SurfaceBulletmarks.Length > 0)
{
GameObject hitPrefab = surfaceDefinition.SurfaceBulletmarks.Random();
GameObject bulletmark = Instantiate(hitPrefab, hitPoint, hitRotation);
bulletmark.transform.SetParent(hit.transform);
}
}
}
private void ShowWeaponTrace(Vector3 localAimDirection)
{
GameObject bullet = Instantiate(bulletAndMuzzleFlash.BulletPrefab, bulletAndMuzzleFlash.BarrelEnd.position, Quaternion.identity);
bullet.GetComponent<Bullet>().SetDirection(localAimDirection, bulletAndMuzzleFlash.BulletForce * 10);
if (bullet.TryGetComponent(out TrailRenderer trail))
trail.material.SetColor("_BaseColor", bulletAndMuzzleFlash.TrailColor);
}
IEnumerator ReloadGun()
{
int bulletsToFullMag = gunProperties.KeepReloadMagBullets
? bulletSettings.BulletsPerMag - bulletsInMag
: bulletSettings.BulletsPerMag;
Animator.SetTrigger(animationSettings.ReloadTrigger);
yield return new WaitForAnimatorStateExit(Animator, animationSettings.GunReloadState);
if (carryingBullets >= bulletsToFullMag)
{
bulletsInMag = bulletSettings.BulletsPerMag;
}
else
{
bulletsInMag = gunProperties.KeepReloadMagBullets
? bulletsInMag += carryingBullets
: carryingBullets;
}
carryingBullets -= bulletsToFullMag;
carryingBullets = (int)Mathf.Clamp(carryingBullets, 0, Mathf.Infinity);
inventory.SetItemQuantity(GunInventoryItem, (ushort)bulletsInMag, false);
inventory.RemoveItemQuantityMany(AmmoInventoryItem, (ushort)bulletsToFullMag);
UpdateAmmoText();
isReloading = false;
fireTime = 0;
}
private void UpdateAmmoText()
{
ammoText.text = string.Format(bulletSettings.AmmoTextFormat, bulletsInMag, carryingBullets);
}
public override bool CanCombine() => !flashlightAttached;
public override void OnItemCombine(InventoryItem combineItem)
{
if (combineItem.ItemGuid != FlashlightAttachmentItem)
return;
inventory.RemoveItem(combineItem);
attachmentSettings.FlashlightAttachment.SetActive(true);
flashlightAttached = true;
}
public override void OnItemSelect()
{
CanvasGroupFader.StartFadeInstance(ammoPanel, true, 5f);
bulletsInMag = inventory.GetItemQuantity(GunInventoryItem);
carryingBullets = inventory.GetAllItemsQuantity(AmmoInventoryItem);
UpdateAmmoText();
ItemObject.SetActive(true);
StartCoroutine(ShowGun());
isEquipped = false;
}
IEnumerator ShowGun()
{
yield return new WaitForAnimatorClip(Animator, animationSettings.GunDrawState);
isEquipped = true;
}
public override void OnItemDeselect()
{
CanvasGroupFader.StartFadeInstance(ammoPanel, false, 5f,
() => ammoPanel.gameObject.SetActive(false));
StopAllCoroutines();
StartCoroutine(HideGun());
Animator.SetTrigger(animationSettings.HideTrigger);
if (flashlightAttached && attachmentSettings.FlashlightLight.enabled)
{
attachmentSettings.FlashlightLight.enabled = false;
audioSource.PlayOneShotSoundClip(attachmentSettings.SwitchSound);
}
flashlightEnabled = false;
isBusy = true;
}
IEnumerator HideGun()
{
yield return new WaitForAnimatorClip(Animator, animationSettings.GunHideState);
ItemObject.SetActive(false);
isEquipped = false;
isBusy = false;
}
public override void OnItemActivate()
{
ammoPanel.alpha = 1f;
ammoPanel.gameObject.SetActive(true);
StopAllCoroutines();
ItemObject.SetActive(true);
Animator.Play(animationSettings.GunIdleState);
bulletsInMag = inventory.GetItemQuantity(GunInventoryItem);
carryingBullets = inventory.GetAllItemsQuantity(AmmoInventoryItem);
isBusy = false;
isEquipped = true;
}
public override void OnItemDeactivate()
{
ammoPanel.alpha = 0f;
ammoPanel.gameObject.SetActive(false);
StopAllCoroutines();
ItemObject.SetActive(false);
if (flashlightAttached && attachmentSettings.FlashlightLight.enabled)
attachmentSettings.FlashlightLight.enabled = false;
isBusy = false;
isEquipped = false;
}
public override StorableCollection OnCustomSave()
{
return new StorableCollection()
{
{ nameof(flashlightAttached), flashlightAttached }
};
}
public override void OnCustomLoad(JToken data)
{
flashlightAttached = (bool)data[nameof(flashlightAttached)];
attachmentSettings.FlashlightAttachment.SetActive(flashlightAttached);
}
public override void OnDrawGizmosSelected()
{
base.OnDrawGizmosSelected();
if (baseSettings.VisualizeDistance)
{
Transform cam = PlayerManager.MainCamera.transform;
Gizmos.color = Color.white;
Gizmos.DrawRay(cam.position, cam.forward * baseSettings.DropoffDistance);
float distanceIte = baseSettings.DropoffDistance / (baseSettings.DropoffSections - 1);
for (int i = 0; i < baseSettings.DropoffSections; i++)
{
float realDistance = distanceIte * i;
float damage = baseSettings.BaseDamage * Mathf.Pow(baseSettings.RangeModifier, realDistance / baseSettings.DropoffDistance);
Vector3 damageDistance = cam.position + cam.forward * realDistance;
Gizmos.color = Color.red.Alpha(0.75f);
Gizmos.DrawSphere(damageDistance, 0.2f);
#if UNITY_EDITOR
UnityEditor.Handles.Label(damageDistance, Mathf.RoundToInt(damage).ToString());
#endif
}
}
}
}
}