using System; using UnityEngine; namespace Arena.Combat { /// /// Weapon that fires projectiles from a muzzle, limited by the fire interval and the ammo count. /// Held by a Handle (usually a hand): snaps to it when enabled, then follows the orientation of the Owner /// while the Owner is active, otherwise the orientation of the Handle. Can be dropped with physics. /// The projectiles ignore the shooter and all its children. /// public class ProjectileLauncher : MonoBehaviour { [System.Serializable] public class Settings { [Tooltip("Weapon data: damage, fire interval, ammo, projectile.")] public WeaponData Weapon; [Tooltip("Team hurt by the projectiles: Enemy for the player, Allied for an enemy, All for everyone.")] public Team Target = Team.All; } [System.Serializable] public struct References { [Tooltip("Spawn point of the projectiles. Its forward axis gives the direction.")] public Transform Muzzle; [Tooltip("Shooter: the projectiles never hit it nor its children, and the weapon follows its orientation while it is active. Empty: the GameObject of this launcher.")] public GameObject Owner; [Tooltip("Transform that holds the weapon, usually a hand. The weapon takes its position and orientation when enabled.")] public Transform Handle; [Tooltip("Rigidbody of the weapon, kinematic while held. Drop makes it fall.")] public Rigidbody Body; } [System.Serializable] public class StateContainer { public int Ammo; [Tooltip("True while an action blocks the shots: running, dash, stun... Set by the owner.")] public bool Disabled; public bool IsDropped; } #region FIELDS [SerializeField] private Settings _settings; [SerializeField] private References _references; [SerializeField] private StateContainer _state; /// /// Raised with the current and max ammo each time the ammo changes. /// public event Action OnAmmoChange; public StateContainer State => _state; public Transform Muzzle => _references.Muzzle; public int MaxAmmo => _settings.Weapon.MaxAmmo; private float _nextFireTime; #endregion #region UNITY LIFECYCLES private void Awake() { _state.Ammo = MaxAmmo; } void OnEnable() { Transform handle = _references.Handle; if (handle && !_state.IsDropped) transform.SetPositionAndRotation(handle.position, handle.rotation); } void LateUpdate() { // LateUpdate: after the animation has moved the hand if (_state.IsDropped) return; GameObject owner = _references.Owner; if (owner && owner.activeInHierarchy) transform.rotation = owner.transform.rotation; else if (_references.Handle) transform.rotation = _references.Handle.rotation; } #endregion #region PUBLIC METHODS /// /// Fires one projectile if the weapon is ready and has ammo. Returns true if a shot was fired. /// public bool TryFire() { if (_state.Disabled || _state.IsDropped || Time.time < _nextFireTime || _state.Ammo <= 0) return false; _nextFireTime = Time.time + _settings.Weapon.FireInterval; SetAmmo(_state.Ammo - 1); Transform muzzle = _references.Muzzle; Projectile projectile = Instantiate(_settings.Weapon.ProjectilePrefab, muzzle.position, muzzle.rotation); GameObject owner = _references.Owner ? _references.Owner : gameObject; projectile.Launch(new DamageInfo(_settings.Weapon.Damage, _settings.Weapon.DamageType, owner, _settings.Target)); return true; } /// /// Blocks or allows the shots. Called by the owner when an action prevents shooting. /// public void SetDisabled(bool isDisabled) { _state.Disabled = isDisabled; } /// /// Lets go of the weapon: it leaves its handle and falls with physics. It can no longer fire. /// public void Drop() { if (_state.IsDropped) return; _state.IsDropped = true; _state.Disabled = true; transform.SetParent(null, true); Rigidbody body = _references.Body; if (!body) { Debug.LogWarning($"{name}: no Rigidbody referenced, the weapon cannot fall.", this); return; } body.isKinematic = false; body.useGravity = true; } /// /// Adds ammo without exceeding the weapon maximum. /// public void AddAmmo(int amount) { if (amount <= 0) return; SetAmmo(Mathf.Min(_state.Ammo + amount, MaxAmmo)); } #endregion #region PRIVATE METHODS private void SetAmmo(int value) { _state.Ammo = value; OnAmmoChange?.Invoke(_state.Ammo, MaxAmmo); } [ContextMenu("Debug/Try Fire")] private void DebugTryFire() { TryFire(); } #endregion } }