using JetBrains.Annotations; using System.Collections; using System.Collections.Generic; using UnityEngine; public class MonsterController : MonoBehaviour { [SerializeField] private SpriteRenderer spriteRenderer; [SerializeField] private Animator animator; [SerializeField] private Rigidbody2D rigidBody; [SerializeField] private DamageController damageController; [SerializeField] private AiController aiController; [SerializeField] private AudioSource audioSource; [SerializeField] private AudioClip attackAudioClip; [SerializeField] private AudioClip defeatAudioClip; private PlayerMovement playerMovement; [SerializeField] private GameObject expEffect; [SerializeField] private GameObject deathEffect; [SerializeField] private GameObject hurtboxEffect; [SerializeField] private GameObject itemObject; [SerializeField] private GameObject moneyObject; [SerializeField] private GameObject hurtboxObject; [SerializeField] public MonsterInfo.MonsterID monsterID; [SerializeField] private float groundCheckRadius; [SerializeField] private float groundCheckHorizontalDistance; [SerializeField] private Transform groundCheckTransform; [SerializeField] private LayerMask groundMask; [SerializeField] private float fadeTime; [SerializeField] private bool enableWalk; [SerializeField] private bool enableHop; [SerializeField] private float minTimeIdle; [SerializeField] private float maxTimeIdle; [SerializeField] private float minTimeWalk; [SerializeField] private float maxTimeWalk; [SerializeField] private float attackTimeActive; [SerializeField] private Vector2 attackEffectOffset = new Vector2(0f, 0f); [SerializeField] private int hopDistance; [SerializeField] private float walkSpeed; [SerializeField] private float chaseMultiplier = 1.0f; [SerializeField] private int fleeDistance = 128; [SerializeField] private int chaseDistance = 48; [Range(0f, 1f)] [SerializeField] private float walkProbability; [Range(0f, 3f)] [SerializeField] private float smoothTime; [SerializeField] private float knockbackVelocity; [SerializeField] private float effectDestroyTime = 3.0f; [SerializeField] private bool hasDashAnimation = false; public bool busy; public bool attacking; public bool isFacingRight; public bool isWalking; public bool isHopping; public bool isSpawning; private Vector2 targetVelocity; private Vector2 referenceVelocity = Vector2.zero; private bool hasStartedDying; private Collider2D[] colliderAllocation; public int numSkillsTakingDamageFrom = 0; public MonsterInfo monsterInfo; // Start is called before the first frame update void Awake() { animator = gameObject.GetComponent(); rigidBody = gameObject.GetComponent(); damageController = gameObject.GetComponent(); aiController = gameObject.GetComponent(); playerMovement = GameObject.FindGameObjectWithTag("Player").GetComponent(); monsterInfo = (MonsterInfo)MonsterInfo.MonsterInfoMap[monsterID]; isFacingRight = true; isWalking = false; isHopping = false; OnSpawnStart(); if (enableWalk) { StartCoroutine("IdleWalkCoroutine"); } else if (enableHop) { StartCoroutine("IdleHopCoroutine"); } } private void FixedUpdate() { if (aiController.closeRangeCollider.IsTouching(aiController.playerColldier)) { } if (!busy && aiController.isPlayerInRange) { // Implement AI behavior. // This overrides the IdleWalk coroutine if the player is in range. if (aiController.intent == AiController.Intent.ATTACK) { // Face the player and attack. if ((transform.position.x < playerMovement.transform.position.x) != isFacingRight) { Flip(); } OnAttackStart(); } else if (aiController.intent == AiController.Intent.CHASE && Mathf.Abs(playerMovement.transform.position.x - transform.position.x) > chaseDistance) { // Run towards the player. // Tries to get within chaseDistance of the player. if (enableWalk) { TryToWalk(transform.position.x < playerMovement.transform.position.x, true); } else if (enableHop && !isHopping) { TryToHop(transform.position.x < playerMovement.transform.position.x - hopDistance); } } else if (aiController.intent == AiController.Intent.FLEE && Mathf.Abs(playerMovement.transform.position.x - transform.position.x) < fleeDistance) { // Run away from the player. // Tries to stay at least fleeDistance away from the player. if (enableWalk) { TryToWalk(transform.position.x > playerMovement.transform.position.x, true); } else if (enableHop && !isHopping) { TryToHop(transform.position.x > playerMovement.transform.position.x + hopDistance); } } else { } } if (enableWalk) {// If you're about to walk off a ledge, don't. bool isMoving = Mathf.Abs(rigidBody.velocity.x) > 0.001; bool isMovingRight = rigidBody.velocity.x > 0; if ((isMoving && !CheckForGround(isMovingRight)) || busy || aiController.isExclaiming || aiController.closeRangeCollider.IsTouching(aiController.playerColldier)) { rigidBody.velocity = new Vector2(0f, rigidBody.velocity.y); targetVelocity = new Vector2(0f, rigidBody.velocity.y); } else if (!isSpawning && Mathf.Abs(rigidBody.velocity.y) < 0.01f) { Vector2 newVelocity = new Vector2(Vector2.SmoothDamp(rigidBody.velocity, targetVelocity, ref referenceVelocity, smoothTime).x, rigidBody.velocity.y); rigidBody.velocity = newVelocity; } animator.SetFloat("Speed", Mathf.Abs(rigidBody.velocity.x)); } } private bool CheckForGround(bool right) { // Circlecast looking for ground objects to your left and right. Vector2 checkPosition; if (right) { checkPosition = new Vector2(groundCheckTransform.position.x + groundCheckHorizontalDistance, groundCheckTransform.position.y); } else { checkPosition = new Vector2(groundCheckTransform.position.x - groundCheckHorizontalDistance, groundCheckTransform.position.y); } colliderAllocation = Physics2D.OverlapCircleAll(checkPosition, groundCheckRadius, groundMask); // int numCollisions = Physics2D.OverlapCircleNonAlloc(checkPosition, groundCheckRadius, colliderAllocation, groundMask); return colliderAllocation.Length > 0; } private bool TryToWalk(bool right, bool chase = false) { if (busy) return false; if (CheckForGround(right)) { if (right) { targetVelocity = new Vector3(walkSpeed * ((chase)?chaseMultiplier:1), 0, 0); } else { targetVelocity = new Vector3(-1 * walkSpeed * ((chase) ? chaseMultiplier : 1), 0, 0); } if (isFacingRight != right) { Flip(); } if (chase && hasDashAnimation) { animator.SetBool("isDashing", true); } return true; } return false; } private IEnumerator IdleWalkCoroutine() { while (true) { if (busy || aiController.isPlayerInRange) { // Don't override. yield return new WaitForSeconds(RandomIdleSeconds()); } else if (ShouldWalk() && !isWalking) { // Walk in a random direction. isWalking = true; if (Random.Range(0f, 1f) < 0.5f) { // Walk left. if (!TryToWalk(false)) { TryToWalk(true); } } else { // Walk right. if (!TryToWalk(true)) { TryToWalk(false); } } yield return new WaitForSeconds(RandomWalkSeconds()); } else { // Idle. isWalking = false; targetVelocity = Vector3.zero; yield return new WaitForSeconds(RandomIdleSeconds()); } } } private IEnumerator IdleHopCoroutine() { while (true) { if (busy || aiController.isPlayerInRange) { // Don't override. yield return new WaitForSeconds(RandomIdleSeconds()); } else if (ShouldWalk() && !isHopping) { // Walk in a random direction. bool right = Random.Range(0f, 1f) < 0.5f; if (!TryToHop(right)) { TryToHop(!right); } yield return new WaitForSeconds(RandomWalkSeconds()); } else { // Idle. targetVelocity = Vector3.zero; yield return new WaitForSeconds(RandomIdleSeconds()); } } } private bool TryToHop(bool right) { if (busy) return false; if (CheckForGround(right)) { isHopping = true; if (right != isFacingRight) { Flip(); } animator.SetBool("isHopping", true); return true; } return false; } private void OnHopEnd() { isHopping = false; animator.SetBool("isHopping", false); transform.position = new Vector2(transform.position.x + hopDistance * (isFacingRight ? 1 : -1), transform.position.y); } private void Flip() { isFacingRight = !isFacingRight; Vector3 scale = transform.localScale; scale.x *= -1; transform.localScale = scale; } private bool ShouldWalk() { return Random.Range(0f, 1f) < walkProbability; } private float RandomIdleSeconds() { return Random.Range(0f, 1f) * (maxTimeIdle - minTimeIdle) + minTimeIdle; } private float RandomWalkSeconds() { return Random.Range(0f, 1f) * (maxTimeWalk - minTimeWalk) + minTimeWalk; } public void OnSpawnStart() { isSpawning = true; busy = true; animator.SetBool("isSpawning", true); } public void OnSpawnEnd() { isSpawning = false; busy = false; animator.SetBool("isSpawning", false); } public void OnHurtStart(bool knockRight) { busy = true; isWalking = false; isHopping = false; if (knockRight && CheckForGround(true)) { rigidBody.velocity = new Vector3(knockbackVelocity, 0, 0); } else if (!knockRight && CheckForGround(false)) { rigidBody.velocity = new Vector3(-1 * knockbackVelocity, 0, 0); } targetVelocity = Vector3.zero; animator.SetBool("isHurting", true); Debug.LogError("Hurting"); } private void OnHurtEnd() { if (damageController.GetHP() < 1) { Debug.LogError("Dying"); OnDieStart(); } else { StartCoroutine("WaitForDamageAnimations", false); } } private void OnDieStart() { if (!hasStartedDying) { hasStartedDying = true; busy = true; aiController.shouldExclaim = false; rigidBody.velocity = Vector3.zero; // Gain EXP. GameObject.FindWithTag("Player").GetComponent().GainExp(damageController.info.exp); // Spawn EXP text. GameObject effect = Instantiate(expEffect); effect.transform.position = rigidBody.transform.position; effect.GetComponent().Setup(damageController.info.exp); // Run the item lottery. for (int i = 0; i < damageController.info.numLotteryRuns; i++) { Item droppedItem = ((Lottery)MonsterInfo.LotteryMap[monsterID]).Roll(damageController.info.minMoney, damageController.info.maxMoney); if (droppedItem != null) { GameObject item; if (droppedItem.subType == ItemSubType.MONEY || droppedItem.subType == ItemSubType.MONEY2 || droppedItem.subType == ItemSubType.MONEY3) { // Spawn money! item = Instantiate(moneyObject); item.GetComponent().Setup(droppedItem); item.transform.position = new Vector3(rigidBody.transform.position.x, rigidBody.transform.position.y - 0.5f); } else { // Spawn an item! item = Instantiate(itemObject); item.GetComponent().Setup(droppedItem); item.transform.position = new Vector3(rigidBody.transform.position.x, rigidBody.transform.position.y - 0.5f); } } } // Update quest completion. HashSet quests = (HashSet)State.state.quests.monsterToQuestID[monsterID]; if (quests != null) { foreach (QuestID questID in quests) { ((QuestProgress)State.state.quests.allQuestProgress[questID]).RegisterMonsterKill(monsterID); } } } StartCoroutine("WaitForDamageAnimations", true); } private IEnumerator WaitForDamageAnimations(bool die = true) { while (numSkillsTakingDamageFrom > 0) { yield return new WaitForEndOfFrame(); } animator.SetBool("isHurting", false); if (damageController.GetHP() < 1) { animator.SetBool("isDead", true); } else { busy = false; } } private void OnDieEnd() { StartCoroutine("FadeOut"); } private void OnAttackStart() { audioSource.PlayOneShot(attackAudioClip); rigidBody.velocity = Vector3.zero; targetVelocity = Vector3.zero; busy = true; attacking = true; isWalking = false; aiController.isCoolingDownAttack = true; Invoke("CompleteCooldown", aiController.attackCooldown); animator.SetBool("isAttacking", true); } private void CompleteCooldown() { aiController.isCoolingDownAttack = false; } private void OnAttackEnd() { busy = false; attacking = false; animator.SetBool("isAttacking", false); } private void SetEffectRelativePosition3D(GameObject effect, float x = 0, float y = 0, float z = 0) { effect.transform.position = new Vector3(transform.position.x + x, transform.position.y + y, transform.position.z + z); } public void SpawnAttack() { GameObject hurtbox = Instantiate(hurtboxObject); SetEffectRelativePosition3D(hurtbox); HurtboxController hbc = hurtbox.GetComponent(); hbc.Setup(attackTimeActive, monsterInfo.attack); if (!isFacingRight) { hbc.transform.localScale = new Vector3(hbc.transform.localScale.x * -1, hbc.transform.localScale.y, hbc.transform.localScale.z); } } public void SpawnAttackEffect() { GameObject effect = Instantiate(hurtboxEffect); Destroy(effect, effectDestroyTime); if (!isFacingRight) { SetEffectRelativePosition3D(effect, -1 * attackEffectOffset.x, attackEffectOffset.y); effect.transform.localScale = new Vector3(effect.transform.localScale.x * -1, effect.transform.localScale.y, effect.transform.localScale.z); } else { SetEffectRelativePosition3D(effect, attackEffectOffset.x, attackEffectOffset.y); } } private void SpawnEffectDie() { audioSource.PlayOneShot(defeatAudioClip); GameObject effect = Instantiate(deathEffect); effect.transform.position = rigidBody.transform.position; if (isFacingRight) { effect.transform.localScale = new Vector3(effect.transform.localScale.x * -1, effect.transform.localScale.y, effect.transform.localScale.z); } } IEnumerator FadeOut() { for (float i = fadeTime; i >= 0; i -= Time.deltaTime) { Color color = spriteRenderer.color; color.a = i / fadeTime; spriteRenderer.color = color; yield return null; } Destroy(gameObject); } }