You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

637 lines
26 KiB
C#

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}
///<summary> Does not shoot projectile when is false, useful for other controllers like Invector and ootii to let them shoot the arrow themselves </summary>
[Tooltip("When the Projectile is Release?")]
public Release_Projectile releaseProjectile = Release_Projectile.OnAttackStart;
///<summary> When Aiming is Cancel what the Weapon should do? </summary>
[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);
/// <summary>This Curve is for Limiting the Bow Animations while the Character is on weird/hard Positions</summary>
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; }
/// <summary> Projectile Instance to launch from the weapon</summary>
public GameObject ProjectileInstance { get; set; }
public Transform ProjectileParent => m_ProjectileParent;
public bool InstantiateProjectileOfFire = true;
public Vector3 Gravity { get => gravity.Value; set => gravity.Value = value; }
/// <summary> Adds a Throw Angle to the Aimer Direction </summary>
public float AimAngle { get => m_AimAngle.Value; set => m_AimAngle.Value = value; }
public Vector3 Velocity { get; set; }
public Action<bool> Predict { get; set; }
/// <summary> Total Ammo of the Weapon</summary>
public int TotalAmmo { get => m_Ammo.Value; set => m_Ammo.Value = value; }
public int AmmoInChamber { get => m_AmmoInChamber.Value; set => m_AmmoInChamber.Value = value; }
/// <summary>When the Ammo in Chamber gets to Zero it will reload Automatically</summary>
public bool AutoReload { get => m_AutoReload.Value; set => m_AutoReload.Value = value; }
public int ChamberSize { get => m_ChamberSize.Value; set => m_ChamberSize.Value = value; }
/// <summary>Does the weapon has Ammo in the Chamber?</summary>
public bool HasAmmo => AmmoInChamber > 0;
/// <summary>Does the weapon has Ammo in the Chamber?</summary>
public float AimWeight { get; private set; }
/// <summary>With Aim Limit?</summary>
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
/// <summary> Set the total Ammo (Refill when you got some ammo) </summary>
public void SetTotalAmmo(int value)
{
if (AutoReload) Reload();
}
public override void WeaponReady(bool value)
{
IsReady = value;
if (IsReady && IsAiming)
{
WeaponAction?.Invoke(WA.Preparing);
}
Debugging($"<color=white><b>[Weapon Ready: {IsReady}] </b></color>", 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($"<color=white> Weapon <b>[Fire Projectile No Charge] </b></color>",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}:<color=white> <b>[Empty Ammo] </b> </color>"); //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}:<color=white> <b>[HACK for the BOW ARROWS] </b> </color>"); //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}:<color=white> <b>[Ammo Reduced({amount})][Total Ammo ({TotalAmmo})][Ammo in Chamber({AmmoInChamber})]</b> </color>"); //Debug
if (AmmoInChamber <= 0 && AutoReload) Reload();
}
/// <summary> Charge the Weapon!! </summary>
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}:<color=white> <b>[Charge Started] </b> [{ProjectileInstance}] </color>"); //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}:<color=white> <b>[Charge Started] </b></color>"); //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);
}
/// <summary> Create an arrow ready to shooot CALLED BY THE ANIMATOR </summary>
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<IProjectile>(); //Get the IArrow Component
var ProjectileRB = ProjectileInstance.GetComponent<Rigidbody>(); //IMPORTANT
if (ProjectileRB)
{
ProjectileRB.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
ProjectileRB.isKinematic = true;
}
var ProjectileCol = ProjectileInstance.GetComponent<Collider>(); //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}:<color=white> <b>[Projectile Equiped] </b> [{ProjectileInstance.name}] </color>"); //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}:<color=white> <b>[Projectile Released] </b> [{ProjectileInstance.name}] </color>"); //Debug
IProjectile projectile = ProjectileInstance.GetComponent<IProjectile>();
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;
}
/// <summary> Destroy the Active Arrow , used when is Stored the Weapon again and it had an arrow ready</summary>
public virtual void DestroyProjectileInstance()
{
if (ProjectileInstance != null)
{
Destroy(ProjectileInstance);
if (debug) Debug.Log($"{name}:<color=white> <b>[Destroy Projectile Inst]</b> </color>"); //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();
}
}
/// <summary> This can be called also by the ANIMATOR </summary>
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;
}
/// <summary> If finish reload but is still aiming go to the Aiming animation **CALLED BY THE ANIMATOR**</summary>
public virtual void FinishReload()
{
WeaponAction?.Invoke(IsAiming && !IsReady ? WA.Aim : WA.Idle);
IsReloading = false;
if (debug) Debug.Log($"{name}:<color=yellow> <b>[Finish Reload]</b> </color>"); //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
}