using UnityEngine;
using UnityEngine.Events;
using MalbersAnimations.Events;
using System.Collections;
using MalbersAnimations.Scriptables;
using System;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace MalbersAnimations.Weapons
{
[AddComponentMenu("Malbers/Weapons/Shootable")]
public class MShootable : MWeapon, IShootableWeapon, IThrower
{
#region Variables
public enum Release_Projectile { Never, OnAttackStart, OnAttackReleased, ByAnimation }
public enum Cancel_Aim { ReleaseProjectile, ResetWeapon}
/// Does not shoot projectile when is false, useful for other controllers like Invector and ootii to let them shoot the arrow themselves
[Tooltip("When the Projectile is Release?")]
public Release_Projectile releaseProjectile = Release_Projectile.OnAttackStart;
/// When Aiming is Cancel what the Weapon should do?
[Tooltip("When the Projectile is Release?")]
public Cancel_Aim CancelAim = Cancel_Aim.ResetWeapon;
[Tooltip("Projectile prefab the weapon fires")]
public GameObjectReference m_Projectile = new GameObjectReference(); //Arrow to Release Prefab
[Tooltip("Parent of the Projectile")]
public Transform m_ProjectileParent;
public Vector3Reference gravity = new Vector3Reference(Physics.gravity);
public BoolReference UseAimAngle = new BoolReference(false);
public BoolReference HasReloadAnim = new BoolReference(false);
public FloatReference m_AimAngle = new FloatReference(0);
/// This Curve is for Limiting the Bow Animations while the Character is on weird/hard Positions
public AnimationCurve AimLimit = new AnimationCurve(new Keyframe[] { new Keyframe(-1, 1), new Keyframe(-0.5f, 1), new Keyframe(0f, 1), new Keyframe(0.5f, 1), new Keyframe(1f, 1) });
[SerializeField] private IntReference m_Ammo = new IntReference(30); //Total of Ammo for this weapon
[SerializeField] private IntReference m_AmmoInChamber = new IntReference(1); //Total of Ammo in the Chamber
[SerializeField] private IntReference m_ChamberSize = new IntReference(1); //Max Capacity of the Ammo in once hit
[SerializeField] private BoolReference m_AutoReload = new BoolReference(false); //Press Fire one or continues
#endregion
#region Events
public GameObjectEvent OnLoadProjectile = new GameObjectEvent();
public GameObjectEvent OnFireProjectile = new GameObjectEvent();
public UnityEvent OnReload = new UnityEvent();
#endregion
#region Properties
public GameObject Projectile { get => m_Projectile.Value; set => m_Projectile.Value = value; }
/// Projectile Instance to launch from the weapon
public GameObject ProjectileInstance { get; set; }
public Transform ProjectileParent => m_ProjectileParent;
public bool InstantiateProjectileOfFire = true;
public Vector3 Gravity { get => gravity.Value; set => gravity.Value = value; }
/// Adds a Throw Angle to the Aimer Direction
public float AimAngle { get => m_AimAngle.Value; set => m_AimAngle.Value = value; }
public Vector3 Velocity { get; set; }
public Action Predict { get; set; }
/// Total Ammo of the Weapon
public int TotalAmmo { get => m_Ammo.Value; set => m_Ammo.Value = value; }
public int AmmoInChamber { get => m_AmmoInChamber.Value; set => m_AmmoInChamber.Value = value; }
/// When the Ammo in Chamber gets to Zero it will reload Automatically
public bool AutoReload { get => m_AutoReload.Value; set => m_AutoReload.Value = value; }
public int ChamberSize { get => m_ChamberSize.Value; set => m_ChamberSize.Value = value; }
/// Does the weapon has Ammo in the Chamber?
public bool HasAmmo => AmmoInChamber > 0;
/// Does the weapon has Ammo in the Chamber?
public float AimWeight { get; private set; }
/// With Aim Limit?
public bool CanShootWithLimit { get; private set; }
public override bool IsEquiped
{
get => base.IsEquiped;
set
{
base.IsEquiped = value;
if (!value)
DestroyProjectileInstance(); //If by AnyChange the Projectile is live Destroy it!!
}
}
public override bool IsAiming
{
get => base.IsAiming;
set
{
base.IsAiming = value;
if (!value)
{
if (CancelAim == Cancel_Aim.ReleaseProjectile) //if the weapon is set to Cancel the Aim
{
MainAttack_Released();
}
else
{
ResetCharge(); //Reset the Charge of the wapon
}
}
}
}
#endregion
/// Set the total Ammo (Refill when you got some ammo)
public void SetTotalAmmo(int value)
{
if (AutoReload) Reload();
}
public override void WeaponReady(bool value)
{
IsReady = value;
if (IsReady && IsAiming)
{
WeaponAction?.Invoke(WA.Preparing);
}
Debugging($"[Weapon Ready: {IsReady}] ", this); //Debug
}
void Awake()
{
Initialize();
}
private void OnEnable()
{
if (!m_Ammo.UseConstant && m_Ammo.Variable != null) //Listen the Total ammo in case it changes
m_Ammo.Variable.OnValueChanged += SetTotalAmmo;
}
private void OnDisable()
{
if (!m_Ammo.UseConstant && m_Ammo.Variable != null)
m_Ammo.Variable.OnValueChanged -= SetTotalAmmo;
}
internal override void OnAnimatorWeaponIK(IMWeaponOwner RC)
{
CalculateAimLimit(RC); //Get te Aim Limit from the Aimer of the Character.
IKProfile?.OnAnimator_IK(RC);
iKProfile?.ApplyOffsets(RC.Anim, RC.AimDirection, AimWeight);
}
internal override void MainAttack_Start(IMWeaponOwner RC)
{
base.MainAttack_Start(RC);
OneChamberAmmo();
if (IsAiming && CanAttack && IsReady) //and the Rider is not on any reload animation
{
if (HasAmmo) //If there's Ammo on the chamber
{
if (!CanCharge) //Means the Weapon does not need to Charge so Release the Projectile First!
{
CalculateAimLimit(RC);
if (CanShootWithLimit)
{
Debugging($" Weapon [Fire Projectile No Charge] ",this); //Debug
WeaponAction.Invoke(WA.Fire_Projectile);
if (releaseProjectile == Release_Projectile.OnAttackStart)
ReleaseProjectile();
}
}
}
else
{
PlaySound(WSound.Empty); //Play Empty Sound Which is stored in the 4 Slot
if (debug) Debug.Log($"{name}: [Empty Ammo] "); //Debug
}
CanAttack = false; //Calcualte the Rate Fire of the arm
}
}
private void OneChamberAmmo()
{
if (!HasAmmo && TotalAmmo > 0 && ChamberSize == 1 && AutoReload)
{
AmmoInChamber = 1; //HACK for 1 Chamber Size Weapon
if (debug) Debug.Log($"{name}: [HACK for the BOW ARROWS] "); //Debug
}
}
internal override void MainAttack_Released()
{
MainInput = false;
if (IsReady && HasAmmo && CanCharge && IsCharging && CanShootWithLimit) //If we are not firing any arrow then try to Attack with the bow
{
WeaponAction?.Invoke(WA.Fire_Projectile); //Play the Fire Animation on the CHaracter
if (releaseProjectile == Release_Projectile.OnAttackReleased)
ReleaseProjectile();
}
}
internal override void SecondaryAttack_Start(IMWeaponOwner RC)
{/*NO WEAPON*/ }
internal override void SecondaryAttack_Released()
{/*NO WEAPON*/ }
public virtual void ReduceAmmo(int amount)
{
AmmoInChamber -= amount;
if (debug) Debug.Log($"{name}: [Ammo Reduced({amount})][Total Ammo ({TotalAmmo})][Ammo in Chamber({AmmoInChamber})] "); //Debug
if (AmmoInChamber <= 0 && AutoReload) Reload();
}
/// Charge the Weapon!!
internal override void Attack_Charge(IMWeaponOwner RC, float time)
{
if (MainInput) //The Input For Charging is Down
{
if (Automatic && CanAttack && Rate > 0) //If is automatic then continue attacking
{
MainAttack_Start(RC);
if (debug) Debug.Log($"{name}: [Charge Started] [{ProjectileInstance}] "); //Debug
}
if (IsReady && HasAmmo && CanCharge) //Is the Weapon ready?? we Have projectiles and we can Charge
{
CalculateAimLimit(RC);
if (!CanShootWithLimit)
{
ResetCharge();
return;
}
if (!IsCharging && IsAiming) //If Attack is pressed Start Bending for more Strength the Bow
{
//WeaponAction?.Invoke(WA.Preparing);
IsCharging = true;
ChargeCurrentTime = 0;
Predict?.Invoke(true);
PlaySound(WSound.Charge); //Play the Charge Sound
if (debug) Debug.Log($"{name}: [Charge Started] "); //Debug
}
else // //If Attack is pressed Continue Bending the Bow for more Strength the Bow
{
Charge(time);
}
}
}
}
private void CalculateAimLimit(IMWeaponOwner RC)
{
AimWeight =
IsRightHanded ? AimLimit.Evaluate(RC.HorizontalAngle) : AimLimit.Evaluate(-RC.HorizontalAngle); //The Weight evaluated on the AnimCurve
CanShootWithLimit = (AimWeight == 1); //Calculate the Imposible range to shoot
}
public override void ResetCharge()
{
base.ResetCharge();
Predict?.Invoke(false);
Velocity = Vector3.zero; //Reset Velocity
}
public override void Charge(float time)
{
base.Charge(time);
CalculateVelocity();
//Predict?.Invoke(true);
}
/// Create an arrow ready to shooot CALLED BY THE ANIMATOR
public virtual void EquipProjectile()
{
if (!HasAmmo) return; //means there's no Ammo
if (ProjectileInstance == null)
{
var Pos = ProjectileParent ? ProjectileParent.position : AimOriginPos;
var Rot = ProjectileParent ? ProjectileParent.rotation : AimOrigin.rotation;
ProjectileInstance = Instantiate(Projectile, Pos, Rot, ProjectileParent); //Instantiate the Arrow in the Knot of the Bow
}
var projectile = ProjectileInstance.GetComponent(); //Get the IArrow Component
var ProjectileRB = ProjectileInstance.GetComponent(); //IMPORTANT
if (ProjectileRB)
{
ProjectileRB.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
ProjectileRB.isKinematic = true;
}
var ProjectileCol = ProjectileInstance.GetComponent(); //IMPORTANT
if (ProjectileCol)
{
ProjectileCol.enabled = false;
}
if (projectile != null)
{
ProjectileInstance.transform.Translate(projectile.PosOffset, Space.Self); //Translate in the offset of the arrow to put it on the hand
ProjectileInstance.transform.Rotate(projectile.RotOffset, Space.Self); //Rotate in the offset of the arrow to put it on the hand
// ProjectileInstance.transform.localScale = (projectile.ScaleOffset); //Scale in the offset of the arrow to put it on the hand
}
OnLoadProjectile.Invoke(ProjectileInstance);
if (debug) Debug.Log($"{name}: [Projectile Equiped] [{ProjectileInstance.name}] "); //Debug
}
public virtual void ReleaseProjectile()
{
if (!gameObject.activeInHierarchy) return; //Crazy bug ??
Predict?.Invoke(false);
if (releaseProjectile == Release_Projectile.Never)
{
DestroyProjectileInstance();
return;
}
else if (InstantiateProjectileOfFire)
{
EquipProjectile();
}
ReduceAmmo(1); //Reduce the Ammo
if (ProjectileInstance == null) return;
ProjectileInstance.transform.parent = null;
if (debug) Debug.Log($"{name}: [Projectile Released] [{ProjectileInstance.name}] "); //Debug
IProjectile projectile = ProjectileInstance.GetComponent();
if (projectile != null)
{
ProjectileInstance.transform.position = AimOrigin.position; //Put the Correct position to Throw the Arrow IMPORTANT!!!!!
CalculateVelocity();
ProjectileInstance.transform.forward = Velocity.normalized; //Align the Projectile to the velocity
ProjectileInstance.transform.Translate(projectile.PosOffset, Space.Self); //Translate in the offset of the arrow to put it on the hand
projectile.Prepare(Owner, Gravity, Velocity, Layer, TriggerInteraction);
if (HitEffect != null) projectile.HitEffect = HitEffect; //Send the Hit Effect too
var newDamage = new StatModifier(statModifier)
{ Value = Mathf.Lerp(MinDamage, MaxDamage, ChargedNormalized) };
projectile.PrepareDamage(newDamage, CriticalChance, CriticalMultiplier);
projectile.Fire();
}
OnFireProjectile.Invoke(ProjectileInstance);
ProjectileInstance = null;
// WeaponReady(false); //Tell the weapon cannot be Ready until Somebody set it ready again
PlaySound(WSound.Fire); //Play the Release Projectile Sound
ResetCharge();
}
private void CalculateVelocity()
{
var Direction = (WeaponOwner.Aimer.AimPoint - AimOrigin.position).normalized;
if (UseAimAngle.Value)
{
var RightV = Vector3.Cross(Direction, -Gravity);
Velocity = Quaternion.AngleAxis(AimAngle, RightV) * Direction * Power;
}
else
Velocity = Direction * Power;
}
/// Destroy the Active Arrow , used when is Stored the Weapon again and it had an arrow ready
public virtual void DestroyProjectileInstance()
{
if (ProjectileInstance != null)
{
Destroy(ProjectileInstance);
if (debug) Debug.Log($"{name}: [Destroy Projectile Inst] "); //Debug
}
ProjectileInstance = null; //Clean the Arrow Instance
}
public override bool Reload()
{
if (TotalAmmo == 0) return false; //Means the Weapon Cannot Reload
if (ChamberSize == AmmoInChamber) return false; //Means there's no need to Reload.. the Chamber is full!!
if (HasReloadAnim.Value)
{
WeaponAction.Invoke(WA.Reload); //PLAY THE RELOAD ANIMATION if you have reload animations; (RELOAD WILL BE DONE VIA ANIMATION)
PlaySound(WSound.Reload);
IsReloading = true;
return true;
}
else
{
return ReloadWeapon();
}
}
/// This can be called also by the ANIMATOR
public bool ReloadWeapon()
{
int RefillChamber = ChamberSize - AmmoInChamber; //Ammo Needed to refill the Chamber
return Reload(RefillChamber);
}
public bool Reload(int ReloadAmount)
{
if ((TotalAmmo == 0) || //Means the Weapon Cannot Reload, there's no more ammo
(ChamberSize == AmmoInChamber)) return false; //Means there's no need to Reload.. the Chamber is full!!
int RefillChamber = ChamberSize - AmmoInChamber; //Ammo Needed to refill the Chamber
ReloadAmount = Mathf.Clamp(ReloadAmount, 0, RefillChamber);
int AmmoLeft = TotalAmmo - ReloadAmount; //Ammo Remaining
if (AmmoLeft >= 0) //If is there any Ammo
{
AmmoInChamber += ReloadAmount;
TotalAmmo -= ReloadAmount;
}
else
{
AmmoInChamber += TotalAmmo; //Set in the Chamber the remaining ammo
TotalAmmo = 0; //Empty the Total Ammo
}
if (ChamberSize <= 1 && TotalAmmo == 0) AmmoInChamber = 0; //Hack to use the AmmoInChamber
OnReload.Invoke();
return true;
}
/// If finish reload but is still aiming go to the Aiming animation **CALLED BY THE ANIMATOR**
public virtual void FinishReload()
{
WeaponAction?.Invoke(IsAiming && !IsReady ? WA.Aim : WA.Idle);
IsReloading = false;
if (debug) Debug.Log($"{name}: [Finish Reload] "); //Debug
}
}
#region INSPECTOR
#if UNITY_EDITOR
[CanEditMultipleObjects, CustomEditor(typeof(MShootable))]
public class MShootableEditor : MWeaponEditor
{
SerializedProperty m_AmmoInChamber, m_Ammo, m_ChamberSize, releaseProjectile, m_Projectile, AimLimit,
m_AutoReload, InstantiateProjectileOfFire, ProjectileParent, CancelAim,
OnReload, OnLoadProjectile, OnFireProjectile, gravity, UseAimAngle, m_AimAngle, HasReloadAnim;
protected MShootable mShoot;
private void OnEnable()
{
SetOnEnable();
mShoot = (MShootable)target;
}
protected override void SetOnEnable()
{
WeaponTab = "Shootable";
base.SetOnEnable();
AimLimit = serializedObject.FindProperty("AimLimit");
UseAimAngle = serializedObject.FindProperty("UseAimAngle");
m_AimAngle = serializedObject.FindProperty("m_AimAngle");
CancelAim = serializedObject.FindProperty("CancelAim");
m_AutoReload = serializedObject.FindProperty("m_AutoReload");
HasReloadAnim = serializedObject.FindProperty("HasReloadAnim");
InstantiateProjectileOfFire = serializedObject.FindProperty("InstantiateProjectileOfFire");
releaseProjectile = serializedObject.FindProperty("releaseProjectile");
m_Projectile = serializedObject.FindProperty("m_Projectile");
ProjectileParent = serializedObject.FindProperty("m_ProjectileParent");
m_AmmoInChamber = serializedObject.FindProperty("m_AmmoInChamber");
m_Ammo = serializedObject.FindProperty("m_Ammo");
m_ChamberSize = serializedObject.FindProperty("m_ChamberSize");
OnReload = serializedObject.FindProperty("OnReload");
OnLoadProjectile = serializedObject.FindProperty("OnLoadProjectile");
OnFireProjectile = serializedObject.FindProperty("OnFireProjectile");
gravity = serializedObject.FindProperty("gravity");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
MalbersEditor.DrawDescription("Projectile Weapons Properties");
WeaponInspector(false);
serializedObject.ApplyModifiedProperties();
}
protected override void UpdateSoundHelp()
{
SoundHelp = "0:Draw 1:Store 2:Shoot 3:Reload 4:Empty 5:Charge";
}
protected override string CustomEventsHelp()
{
return "\n\n On Fire Gun: Invoked when the weapon is fired \n(Vector3: the Aim direction of the rider), \n\n On Hit: Invoked when the Weapon Fired and hit something \n(Transform: the gameobject that was hitted) \n\n On Aiming: Invoked when the Rider is Aiming or not \n\n On Reload: Invoked when Reload";
}
protected override void DrawExtras()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
{
minForce.isExpanded = MalbersEditor.Foldout(minForce.isExpanded, "Physics Force");
if (minForce.isExpanded)
{
EditorGUILayout.PropertyField(minForce, new GUIContent("Min", "Minimun Force to apply to a hitted rigid body"));
EditorGUILayout.PropertyField(Force, new GUIContent("Max", "Maximun Force to apply to a hitted rigid body"));
EditorGUILayout.PropertyField(forceMode);
EditorGUILayout.PropertyField(gravity);
}
}
EditorGUILayout.EndVertical();
DrawMisc();
}
protected override void DrawAdvancedWeapon()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Aim Properties", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_AimOrigin);
EditorGUILayout.PropertyField(m_AimSide);
EditorGUILayout.PropertyField(CancelAim);
EditorGUILayout.PropertyField(AimLimit, new GUIContent("Aim Limit", "This Curve is for Limiting the Bow Animations while the Character is on weird/hard Positions"));
EditorGUILayout.PropertyField(UseAimAngle, new GUIContent("Use Aim Angle", " Adds a Throw Angle to the Aimer Direction?"));
if (mShoot.UseAimAngle.Value)
{
EditorGUILayout.PropertyField(m_AimAngle, new GUIContent("Aim Angle", " Adds a Throw Angle to the Aimer Direction"));
}
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Projectile", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(releaseProjectile);
if (releaseProjectile.intValue != 0)
{
EditorGUILayout.PropertyField(InstantiateProjectileOfFire, new GUIContent("Inst Projectile on Fire", "Instanciate the Projectile when Firing the weapon.\n E.g The Pistol Instantiate the projectile on Firing. The bow Instantiate the Arrow Before Firing"));
EditorGUILayout.PropertyField(m_Projectile);
EditorGUILayout.PropertyField(ProjectileParent);
}
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Ammunition", EditorStyles.boldLabel);
//EditorGUILayout.PropertyField(m_Automatic, new GUIContent("Automatic", "one shot at the time or Automatic"));
EditorGUILayout.PropertyField(m_AutoReload, new GUIContent("Auto Reload", "The weapon will reload automatically when the Ammo in chamber is zero"));
EditorGUILayout.PropertyField(HasReloadAnim, new GUIContent("Has Reload Anim", "If the Weapon have reload animation then Play it"));
EditorGUILayout.PropertyField(m_ChamberSize, new GUIContent("Chamber Size", "Total of Ammo that can be shoot before reloading"));
if (mShoot.ChamberSize > 1)
{
EditorGUILayout.PropertyField(m_AmmoInChamber, new GUIContent("Ammo in Chamber", "Current ammo in the chamber"));
}
EditorGUILayout.PropertyField(m_Ammo, new GUIContent("Total Ammo", "Total ammo for the weapon"));
EditorGUILayout.EndVertical();
}
protected override void ChildWeaponEvents()
{
EditorGUILayout.PropertyField(OnLoadProjectile);
EditorGUILayout.PropertyField(OnFireProjectile);
//EditorGUILayout.PropertyField(OnAiming);
EditorGUILayout.PropertyField(OnReload);
}
}
#endif
#endregion
}