EscapeShooter/My project/Assets/Project Files/Game/Scripts/Enemy/BaseEnemyBehaviour.cs

599 lines
18 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using Watermelon.LevelSystem;
using Random = UnityEngine.Random;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
namespace Watermelon.SquadShooter
{
public abstract class BaseEnemyBehavior : MonoBehaviour, IHealth, INavMeshAgent
{
private static readonly Color HIT_OVERLAY_COLOR = new Color(0.6f, 0.6f, 0.6f, 1.0f);
protected readonly int ANIMATOR_RUN_HASH = Animator.StringToHash("Running");
protected readonly int ANIMATOR_SPEED_HASH = Animator.StringToHash("Movement Speed");
public static readonly int ANIMATOR_HIT_HASH = Animator.StringToHash("Hit");
public static readonly int ANIMATOR_HIT_INDEX_HASH = Animator.StringToHash("Hit Index");
public static readonly float ANIMATOR_HIT_COOLDOWN = 0.08f;
private static readonly int SHADER_HIT_SHINE_COLOR_HASH = Shader.PropertyToID("_EmissionColor");
private static readonly int MASK_PERCENT_HASH = Shader.PropertyToID("_MaskPercent");
[SerializeField] EnemyType type;
public EnemyType EnemyType => type;
[SerializeField] EnemyTier tier = EnemyTier.Regular;
public EnemyTier Tier { get => tier; private set => tier = value; }
protected EnemyStats stats;
public EnemyStats Stats => stats;
[SerializeField]
protected Animator animatorRef;
public Animator Animator => animatorRef;
[SerializeField]
protected HealthbarBehaviour healthbarBehaviour;
[SerializeField] EnemyAnimationCallback enemyAnimationCallback;
[Space]
[SerializeField]
protected SkinnedMeshRenderer meshRenderer;
public bool IsVisible => meshRenderer.isVisible;
[SerializeField]
protected Collider baseHitCollider;
protected IStateMachine StateMachine { get; private set; }
[Space]
[SerializeField] EliteCase eliteCase;
[Header("Weapon")]
[SerializeField] List<WeaponRigBehavior> weapons;
[SerializeField] Transform rightHandBone;
[SerializeField] Transform leftHandBone;
public Transform RightHandBone => rightHandBone;
public Transform LeftHandBone => leftHandBone;
public float deathExplosionForce = 7000;
public float deathExplosionRadius = 100;
public bool CanPursue { get; set; }
public bool CanMove { get; set; }
public bool IsAttacking { get; set; }
// Health
protected float currentHealth;
public float CurrentHealth => currentHealth;
public float MaxHealth => stats.Hp * (tier == EnemyTier.Elite ? stats.EliteHealthMult : 1f);
protected bool isDead;
public bool IsDead => isDead;
protected bool chaseMode; // activates after first hit - never stop chasing target
// movement
protected NavMeshAgent navMeshAgent;
public NavMeshAgent NavMeshAgent => navMeshAgent;
public float RunningSpeed { get; protected set; }
public float WalkingSpeed { get; protected set; }
public bool IsWalking { get; set; }
public Vector3 Position => transform.position;
public Quaternion Rotation { get => transform.rotation; set => transform.rotation = value; }
public Vector3 TargetPosition => Target.position;
public float VisionRange => visionRange;
public bool IsTargetInVisionRange => Vector3.Distance(transform.position, Target.position) <= visionRange;
public bool IsTargetInAttackRange => Vector3.Distance(transform.position, Target.position) <= stats.AttackDistance;
public bool IsTargetInFleeRange => Vector3.Distance(transform.position, Target.position) <= stats.FleeDistance;
public bool HasTakenDamage { get; private set; }
protected Transform target;
public Transform Target => target;
protected CharacterBehaviour characterBehaviour;
protected CapsuleCollider enemyCollider;
protected Rigidbody enemyRigidbody;
public static OnEnemyDiedDelegate OnDiedEvent;
private MaterialPropertyBlock hitShinePropertyBlock;
private TweenCase hitShineTweenCase;
private float lastFlotingTextTime;
private float lastDamagedTime;
private float lastHitShineTime;
protected float hitAnimationTime;
public HealthbarBehaviour HealthbarBehaviour => healthbarBehaviour;
private Vector3[] patrollingPoints;
public Vector3[] PatrollingPoints => patrollingPoints;
private List<DropData> dropData;
protected EnemyData enemyData;
protected float visionRange;
protected TweenCase ragdollCase;
private float hitOffsetMult;
// Ragdoll
protected RagdollBehavior ragdoll;
public RagdollBehavior Ragdoll => ragdoll;
// Behavior
public event SimpleCallback OnTakenDamage;
public event SimpleCallback OnReloadFinished;
protected Vector3 lastProjectilePosition;
public event SimpleCallback OnAttackFinished;
public delegate void OnEnemyDiedDelegate(BaseEnemyBehavior enemy);
protected virtual void Awake()
{
isDead = true;
navMeshAgent = GetComponent<NavMeshAgent>();
enemyCollider = GetComponent<CapsuleCollider>();
enemyRigidbody = GetComponent<Rigidbody>();
ragdoll = new RagdollBehavior();
ragdoll.Init(transform);
hitShinePropertyBlock = new MaterialPropertyBlock();
enemyAnimationCallback.Init(this);
navMeshAgent.enabled = false;
RunningSpeed = navMeshAgent.speed;
WalkingSpeed = RunningSpeed / 2f;
if (baseHitCollider)
baseHitCollider.enabled = true;
StateMachine = GetComponent<IStateMachine>();
for (int i = 0; i < weapons.Count; i++)
{
weapons[i].Init(this);
}
}
public void SetEnemyData(EnemyData enemyData, bool isElite)
{
eliteCase.Validate();
if (isElite)
{
Tier = EnemyTier.Elite;
eliteCase.SetElite();
}
else if (Tier != EnemyTier.Boss)
{
Tier = EnemyTier.Regular;
eliteCase.SetRegular();
}
this.enemyData = enemyData;
stats = enemyData.Stats;
visionRange = enemyData.Stats.VisionRange;
}
public virtual void Init()
{
characterBehaviour = CharacterBehaviour.GetBehaviour();
target = characterBehaviour.transform;
hitOffsetMult = 1f;
transform.localScale = Vector3.one;
// Disable rigidbody
enemyRigidbody.isKinematic = true;
enemyRigidbody.useGravity = false;
enemyCollider.isTrigger = true;
animatorRef.gameObject.SetActive(true);
// health
currentHealth = MaxHealth;
// Initialise healthbar
healthbarBehaviour.Init(transform, this, true, healthbarBehaviour.HealthbarOffset, LevelController.CurrentLevelData.EnemiesLevel, Tier == EnemyTier.Elite);
isDead = false;
chaseMode = false;
NavMeshController.InvokeOrSubscribe(this);
LevelController.OnPlayerDiedEvent += OnRoomDone;
LevelController.OnPlayerExitLevelEvent += OnRoomDone;
baseHitCollider.enabled = true;
HasTakenDamage = false;
StateMachine.StartMachine();
for (int i = 0; i < weapons.Count; i++)
{
if (weapons[i] != null) weapons[i].enabled = true;
}
}
public virtual void OnRoomDone()
{
LevelController.OnPlayerDiedEvent -= OnRoomDone;
LevelController.OnPlayerExitLevelEvent -= OnRoomDone;
ragdollCase.KillActive();
}
public void OnNavMeshUpdated()
{
navMeshAgent.enabled = true;
navMeshAgent.stoppingDistance = stats.PreferedDistanceToPlayer;
navMeshAgent.speed = stats.MoveSpeed;
navMeshAgent.angularSpeed = stats.AngularSpeed;
}
public abstract void OnAnimatorCallback(EnemyCallbackType enemyCallbackType);
protected virtual void OnDestroy()
{
if (healthbarBehaviour != null && healthbarBehaviour.HealthBarTransform != null)
{
Destroy(healthbarBehaviour.HealthBarTransform.gameObject);
}
}
public virtual void Unload()
{
healthbarBehaviour.Destroy();
if (navMeshAgent.isActiveAndEnabled)
navMeshAgent.isStopped = true;
}
public bool IsTargetInSight()
{
var origin = transform.position.SetY(1);
var ray = new Ray(origin, (Target.position.SetY(1) - origin).normalized);
if (Physics.Raycast(ray, out var hit, Stats.AttackDistance, 328))
{
if (hit.collider.gameObject == Target.gameObject)
return true;
}
return false;
}
private void OnValidate()
{
if (weapons == null) return;
for (int i = 0; i < weapons.Count; i++)
{
var weapon = weapons[i];
if (weapon == null) continue;
weapon.Init(this);
}
}
#region Combat
public int GetCurrentDamage()
{
return (int)(stats.Damage.Random() * (tier == EnemyTier.Elite ? stats.EliteDamageMult : 1f));
}
public void StartChasing()
{
visionRange = 9999;
}
protected void InvokeOnReloadFinished()
{
OnReloadFinished?.Invoke();
}
public virtual void TakeDamage(float damage, Vector3 projectilePosition, Vector3 projectileDirection)
{
if (isDead) return;
if (damage <= 0) return;
currentHealth = Mathf.Clamp(currentHealth - damage, 0, MaxHealth);
healthbarBehaviour.OnHealthChanged();
lastProjectilePosition = projectilePosition - projectileDirection;
if (currentHealth <= 0)
OnDeath();
transform.position += (transform.position - target.position).normalized * 0.15f * hitOffsetMult;
hitOffsetMult *= 0.8f;
HitEffect();
if (lastFlotingTextTime + 0.18f <= Time.realtimeSinceStartup)
{
lastFlotingTextTime = Time.realtimeSinceStartup;
FloatingTextController.SpawnFloatingText("Hit", "-" + damage.ToString("F0"), transform.position + transform.forward * stats.HitTextOffsetForward + new Vector3(Random.Range(-0.3f, 0.3f), stats.HitTextOffsetY, Random.Range(-0.1f, 0.1f)), Quaternion.identity, 1.0f, Color.white);
}
lastDamagedTime = Time.realtimeSinceStartup;
OnTakenDamage?.Invoke();
HasTakenDamage = true;
}
protected virtual void FixedUpdate()
{
if (hitOffsetMult < 1 && lastDamagedTime + 0.5f < Time.realtimeSinceStartup)
{
hitOffsetMult += Time.fixedDeltaTime;
}
}
protected virtual void OnDeath()
{
isDead = true;
navMeshAgent.enabled = false;
// Disable healthbar
healthbarBehaviour.DisableBar();
enemyCollider.isTrigger = false;
animatorRef.enabled = false;
ShowDeathFallAnimation();
if (baseHitCollider)
baseHitCollider.enabled = false;
ActivateRagdollOnDeath();
DropResources();
if(Random.Range(0,2) == 0)
{
AudioController.PlaySound(AudioController.GetClip("enemy_hit_1"));
}
else
{
AudioController.PlaySound(AudioController.GetClip("enemy_hit_2"));
}
LevelController.OnEnemyKilled(this);
OnDiedEvent?.Invoke(this);
HasTakenDamage = false;
StateMachine.StopMachine();
}
protected void ActivateRagdollOnDeath()
{
for (int i = 0; i < weapons.Count; i++)
{
if (weapons[i] != null) weapons[i].enabled = false;
}
EnableRagdoll(deathExplosionForce, lastProjectilePosition);
ragdollCase = Tween.DelayedCall(2.0f, () =>
{
ragdoll?.Disable();
});
}
private void EnableRagdoll(float force, Vector3 point)
{
ragdoll?.ActivateWithForce(point, force, deathExplosionRadius);
}
private void ShowDeathFallAnimation()
{
// Enable rigidbody
enemyRigidbody.isKinematic = true;
enemyRigidbody.useGravity = false;
enemyRigidbody.AddExplosionForce(deathExplosionForce, transform.position + (transform.forward * 0.5f).SetY(15f) + transform.right * Random.Range(-0.5f, 0.5f), deathExplosionRadius);
}
protected void HitEffect()
{
if (lastHitShineTime + 0.11f > Time.realtimeSinceStartup)
return;
lastHitShineTime = Time.realtimeSinceStartup;
hitShineTweenCase.KillActive();
meshRenderer.GetPropertyBlock(hitShinePropertyBlock);
hitShinePropertyBlock.SetColor(SHADER_HIT_SHINE_COLOR_HASH, HIT_OVERLAY_COLOR);
meshRenderer.SetPropertyBlock(hitShinePropertyBlock);
hitShineTweenCase = meshRenderer.DOPropertyBlockColor(SHADER_HIT_SHINE_COLOR_HASH, hitShinePropertyBlock, Color.black, 0.32f);
}
protected void SetMaskPercent(float percent)
{
meshRenderer.GetPropertyBlock(hitShinePropertyBlock);
hitShinePropertyBlock.SetFloat(MASK_PERCENT_HASH, percent);
meshRenderer.SetPropertyBlock(hitShinePropertyBlock);
}
protected void HitAnimation(int animationIndex)
{
animatorRef.SetTrigger(ANIMATOR_HIT_HASH);
animatorRef.SetInteger(ANIMATOR_HIT_INDEX_HASH, animationIndex);
hitAnimationTime = Time.time + ANIMATOR_HIT_COOLDOWN;
}
public abstract void Attack();
protected void InvokeOnAttackFinished()
{
OnAttackFinished?.Invoke();
}
#endregion
#region Drop
public void ResetDrop()
{
dropData = new List<DropData>();
}
public void AddDrop(DropData drop)
{
dropData.Add(drop);
}
protected void DropResources()
{
if (!LevelController.IsGameplayActive)
return;
if (!dropData.IsNullOrEmpty())
{
for (int i = 0; i < dropData.Count; i++)
{
Drop.SpawnDropItem(dropData[i], transform.position, new Vector3(0, Random.Range(0f, 360f), 0), false, (drop, fallingStyle) =>
{
Drop.ThrowItem(drop, fallingStyle);
});
}
}
if (Random.Range(0.0f, 1.0f) <= ActiveRoom.LevelData.HealSpawnPercent)
{
int health = Mathf.RoundToInt(Stats.HpForPlayer.Random());
Drop.SpawnDropItem(new DropData() { DropType = DropableItemType.Heal, Amount = health }, transform.position, new Vector3(0, Random.Range(0f, 360f), 0), false, (drop, fallingStyle) =>
{
Drop.ThrowItem(drop, fallingStyle);
});
}
}
#endregion
#region Movement
public void MoveToPoint(Vector3 pos)
{
if (navMeshAgent != null)
{
navMeshAgent.isStopped = false;
navMeshAgent.SetDestination(pos);
}
}
public void StopMoving()
{
if (gameObject.activeInHierarchy && navMeshAgent.enabled && navMeshAgent.isOnNavMesh)
navMeshAgent.isStopped = true;
}
public void SetAnimMovementMultiplier(float speedMultiplier)
{
animatorRef.SetFloat(ANIMATOR_SPEED_HASH, navMeshAgent.velocity.magnitude / RunningSpeed * speedMultiplier);
}
public void SetPatrollingPoints(Vector3[] points)
{
patrollingPoints = points;
}
#endregion
#if UNITY_EDITOR
[Button("Toggle Animation Mode")]
private void ToggleAnimationMode()
{
if (AnimationMode.InAnimationMode())
{
AnimationMode.StopAnimationMode();
PrefabStage.prefabStageClosing -= OnPrefabClosing;
}
else
{
AnimationMode.StartAnimationMode();
PrefabStage.prefabStageClosing += OnPrefabClosing;
}
}
private void OnPrefabClosing(PrefabStage obj)
{
AnimationMode.StopAnimationMode();
PrefabStage.prefabStageClosing -= OnPrefabClosing;
}
[Button("Sample Random Animation", "IsAnimationMode", ButtonVisibility.ShowIf)]
private void SampleRandomAnimation()
{
if (AnimationMode.InAnimationMode())
{
var randomClip = animatorRef.runtimeAnimatorController.animationClips.GetRandomItem();
AnimationMode.BeginSampling();
AnimationMode.SampleAnimationClip(animatorRef.gameObject, randomClip, Random.value);
AnimationMode.EndSampling();
}
else
{
Debug.LogError("Animation Mode is turned off");
}
}
private void OnDrawGizmos()
{
if (AnimationMode.InAnimationMode())
{
GUIStyle style = new GUIStyle();
style.fontSize = 30;
style.fontStyle = FontStyle.Bold;
style.normal.textColor = Color.red;
Handles.Label(transform.position, "ANIMATION MODE", style);
}
}
protected bool IsAnimationMode()
{
return AnimationMode.InAnimationMode();
}
#endif
}
}