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 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; 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(); enemyCollider = GetComponent(); enemyRigidbody = GetComponent(); 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(); 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(); } 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 } }