308 lines
8.0 KiB
C#
308 lines
8.0 KiB
C#
|
|
using System.Collections;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.UI;
|
||
|
|
|
||
|
|
public class Weapon : MonoBehaviour
|
||
|
|
{
|
||
|
|
public WeaponData data;
|
||
|
|
public Text ammoText;
|
||
|
|
public Text weaponNameText;
|
||
|
|
public Text reloadText;
|
||
|
|
public Transform firePoint;
|
||
|
|
public Camera playerCamera;
|
||
|
|
public WeaponHUD hud;
|
||
|
|
|
||
|
|
private int currentAmmo;
|
||
|
|
private int currentReserve;
|
||
|
|
private float nextFireTime;
|
||
|
|
private bool isReloading;
|
||
|
|
private Vector3 originalLocalPosition;
|
||
|
|
private Quaternion originalCameraRotation;
|
||
|
|
|
||
|
|
private bool initialized;
|
||
|
|
private AudioSource audioSource;
|
||
|
|
private Coroutine reloadRoutine;
|
||
|
|
private Coroutine recoilRoutine;
|
||
|
|
private SimpleFPSController fpsController;
|
||
|
|
|
||
|
|
private void Awake()
|
||
|
|
{
|
||
|
|
audioSource = GetComponent<AudioSource>();
|
||
|
|
originalLocalPosition = transform.localPosition;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnEnable()
|
||
|
|
{
|
||
|
|
if (Application.isPlaying)
|
||
|
|
OnSelected();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Update()
|
||
|
|
{
|
||
|
|
if (data == null)
|
||
|
|
return;
|
||
|
|
|
||
|
|
bool wantsToShoot = data.automatic ? Input.GetButton("Fire1") : Input.GetButtonDown("Fire1");
|
||
|
|
|
||
|
|
if (wantsToShoot)
|
||
|
|
TryShoot();
|
||
|
|
|
||
|
|
if (Input.GetKeyDown(KeyCode.R))
|
||
|
|
Reload();
|
||
|
|
|
||
|
|
UpdateUI();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Initialize()
|
||
|
|
{
|
||
|
|
if (data == null || initialized)
|
||
|
|
return;
|
||
|
|
|
||
|
|
currentAmmo = Mathf.Max(0, data.magazineSize);
|
||
|
|
currentReserve = Mathf.Max(0, data.totalReserveAmmo);
|
||
|
|
originalLocalPosition = transform.localPosition;
|
||
|
|
|
||
|
|
if (playerCamera != null)
|
||
|
|
{
|
||
|
|
originalCameraRotation = playerCamera.transform.localRotation;
|
||
|
|
fpsController = playerCamera.GetComponentInParent<SimpleFPSController>();
|
||
|
|
}
|
||
|
|
|
||
|
|
initialized = true;
|
||
|
|
UpdateUI();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void TryShoot()
|
||
|
|
{
|
||
|
|
if (data == null)
|
||
|
|
return;
|
||
|
|
|
||
|
|
// Shooting behavior is data-driven: automatic weapons use held Fire1, semi-auto weapons use one click.
|
||
|
|
if (isReloading)
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (currentAmmo <= 0)
|
||
|
|
{
|
||
|
|
ShowMessage("No ammo. Press R to reload.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Time.time < nextFireTime)
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (!CanShoot())
|
||
|
|
return;
|
||
|
|
|
||
|
|
Shoot();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Shoot()
|
||
|
|
{
|
||
|
|
if (data == null || playerCamera == null)
|
||
|
|
return;
|
||
|
|
|
||
|
|
currentAmmo--;
|
||
|
|
nextFireTime = Time.time + Mathf.Max(0.01f, data.fireRate);
|
||
|
|
|
||
|
|
PlaySound(data.shootSound);
|
||
|
|
SpawnMuzzleFlash();
|
||
|
|
|
||
|
|
int pelletCount = Mathf.Max(1, data.pelletsPerShot);
|
||
|
|
for (int i = 0; i < pelletCount; i++)
|
||
|
|
FireRay();
|
||
|
|
|
||
|
|
ApplyRecoil();
|
||
|
|
UpdateUI();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Reload()
|
||
|
|
{
|
||
|
|
if (data == null || isReloading)
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (currentAmmo >= data.magazineSize)
|
||
|
|
{
|
||
|
|
ShowMessage("Magazine is full.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (currentReserve <= 0)
|
||
|
|
{
|
||
|
|
ShowMessage("No reserve ammo.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
reloadRoutine = StartCoroutine(ReloadRoutine());
|
||
|
|
}
|
||
|
|
|
||
|
|
public void UpdateUI()
|
||
|
|
{
|
||
|
|
if (data != null && weaponNameText != null)
|
||
|
|
weaponNameText.text = "Weapon: " + data.weaponName;
|
||
|
|
|
||
|
|
if (ammoText != null)
|
||
|
|
ammoText.text = "Ammo: " + currentAmmo + " / " + currentReserve;
|
||
|
|
|
||
|
|
if (reloadText != null)
|
||
|
|
reloadText.text = isReloading ? "Reloading..." : "Ready";
|
||
|
|
|
||
|
|
if (hud != null)
|
||
|
|
{
|
||
|
|
hud.SetWeapon(data);
|
||
|
|
hud.SetAmmo(currentAmmo, currentReserve);
|
||
|
|
hud.SetReloading(isReloading);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void OnSelected()
|
||
|
|
{
|
||
|
|
Initialize();
|
||
|
|
UpdateUI();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void OnDeselected()
|
||
|
|
{
|
||
|
|
if (recoilRoutine != null)
|
||
|
|
{
|
||
|
|
StopCoroutine(recoilRoutine);
|
||
|
|
recoilRoutine = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
transform.localPosition = originalLocalPosition;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool CanShoot()
|
||
|
|
{
|
||
|
|
return data != null && !isReloading && currentAmmo > 0 && Time.time >= nextFireTime;
|
||
|
|
}
|
||
|
|
|
||
|
|
public int GetCurrentAmmo()
|
||
|
|
{
|
||
|
|
return currentAmmo;
|
||
|
|
}
|
||
|
|
|
||
|
|
public int GetCurrentReserve()
|
||
|
|
{
|
||
|
|
return currentReserve;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool IsReloading()
|
||
|
|
{
|
||
|
|
return isReloading;
|
||
|
|
}
|
||
|
|
|
||
|
|
private IEnumerator ReloadRoutine()
|
||
|
|
{
|
||
|
|
isReloading = true;
|
||
|
|
UpdateUI();
|
||
|
|
PlaySound(data.reloadSound);
|
||
|
|
|
||
|
|
yield return new WaitForSeconds(Mathf.Max(0f, data.reloadTime));
|
||
|
|
|
||
|
|
int neededAmmo = Mathf.Max(0, data.magazineSize - currentAmmo);
|
||
|
|
int ammoToLoad = Mathf.Min(neededAmmo, currentReserve);
|
||
|
|
|
||
|
|
currentAmmo += ammoToLoad;
|
||
|
|
currentReserve -= ammoToLoad;
|
||
|
|
isReloading = false;
|
||
|
|
reloadRoutine = null;
|
||
|
|
UpdateUI();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void FireRay()
|
||
|
|
{
|
||
|
|
Vector3 origin = playerCamera.transform.position;
|
||
|
|
|
||
|
|
// Spread is applied around the camera forward vector, so all weapons use the same aiming source.
|
||
|
|
Vector2 randomOffset = Random.insideUnitCircle * Mathf.Max(0f, data.spread);
|
||
|
|
Vector3 direction = playerCamera.transform.forward +
|
||
|
|
playerCamera.transform.right * randomOffset.x +
|
||
|
|
playerCamera.transform.up * randomOffset.y;
|
||
|
|
direction.Normalize();
|
||
|
|
|
||
|
|
Debug.DrawRay(origin, direction * data.range, Color.yellow, 1f);
|
||
|
|
|
||
|
|
if (Physics.Raycast(origin, direction, out RaycastHit hit, Mathf.Max(0.1f, data.range)))
|
||
|
|
{
|
||
|
|
EnemyTarget target = hit.collider.GetComponentInParent<EnemyTarget>();
|
||
|
|
if (target != null)
|
||
|
|
target.TakeDamage(data.damage);
|
||
|
|
|
||
|
|
SpawnHitEffect(hit);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ApplyRecoil()
|
||
|
|
{
|
||
|
|
// The FPS controller owns camera look; the weapon only feeds a temporary visual recoil offset.
|
||
|
|
if (fpsController != null)
|
||
|
|
fpsController.AddRecoil(data.recoilForce);
|
||
|
|
else if (playerCamera != null)
|
||
|
|
playerCamera.transform.localRotation = originalCameraRotation * Quaternion.Euler(-data.recoilForce, 0f, 0f);
|
||
|
|
|
||
|
|
if (recoilRoutine != null)
|
||
|
|
StopCoroutine(recoilRoutine);
|
||
|
|
|
||
|
|
recoilRoutine = StartCoroutine(RecoilRoutine());
|
||
|
|
}
|
||
|
|
|
||
|
|
private IEnumerator RecoilRoutine()
|
||
|
|
{
|
||
|
|
Vector3 recoilPosition = originalLocalPosition - Vector3.forward * Mathf.Clamp(data.recoilForce * 0.035f, 0.02f, 0.18f);
|
||
|
|
float t = 0f;
|
||
|
|
|
||
|
|
while (t < 1f)
|
||
|
|
{
|
||
|
|
t += Time.deltaTime * 20f;
|
||
|
|
transform.localPosition = Vector3.Lerp(transform.localPosition, recoilPosition, t);
|
||
|
|
yield return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
t = 0f;
|
||
|
|
while (t < 1f)
|
||
|
|
{
|
||
|
|
t += Time.deltaTime * 10f;
|
||
|
|
transform.localPosition = Vector3.Lerp(transform.localPosition, originalLocalPosition, t);
|
||
|
|
|
||
|
|
if (fpsController == null && playerCamera != null)
|
||
|
|
playerCamera.transform.localRotation = Quaternion.Slerp(playerCamera.transform.localRotation, originalCameraRotation, t);
|
||
|
|
|
||
|
|
yield return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
transform.localPosition = originalLocalPosition;
|
||
|
|
recoilRoutine = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void SpawnMuzzleFlash()
|
||
|
|
{
|
||
|
|
if (data.muzzleFlashPrefab == null || firePoint == null)
|
||
|
|
return;
|
||
|
|
|
||
|
|
GameObject flash = Instantiate(data.muzzleFlashPrefab, firePoint.position, firePoint.rotation, firePoint);
|
||
|
|
Destroy(flash, 0.08f);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void SpawnHitEffect(RaycastHit hit)
|
||
|
|
{
|
||
|
|
if (data.hitEffectPrefab == null)
|
||
|
|
return;
|
||
|
|
|
||
|
|
Quaternion rotation = Quaternion.LookRotation(hit.normal);
|
||
|
|
GameObject effect = Instantiate(data.hitEffectPrefab, hit.point + hit.normal * 0.01f, rotation);
|
||
|
|
Destroy(effect, 0.8f);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void PlaySound(AudioClip clip)
|
||
|
|
{
|
||
|
|
if (clip != null && audioSource != null)
|
||
|
|
audioSource.PlayOneShot(clip);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ShowMessage(string message)
|
||
|
|
{
|
||
|
|
if (hud != null)
|
||
|
|
hud.ShowMessage(message);
|
||
|
|
}
|
||
|
|
}
|