448 lines
17 KiB
C#
448 lines
17 KiB
C#
|
|
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<Animator>();
|
|||
|
|
rigidBody = gameObject.GetComponent<Rigidbody2D>();
|
|||
|
|
damageController = gameObject.GetComponent<DamageController>();
|
|||
|
|
aiController = gameObject.GetComponent<AiController>();
|
|||
|
|
playerMovement = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerMovement>();
|
|||
|
|
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<PlayerMovement>().GainExp(damageController.info.exp);
|
|||
|
|
// Spawn EXP text.
|
|||
|
|
GameObject effect = Instantiate(expEffect);
|
|||
|
|
effect.transform.position = rigidBody.transform.position;
|
|||
|
|
effect.GetComponent<ExpTextController>().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<MoneyController>().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<ItemController>().Setup(droppedItem);
|
|||
|
|
item.transform.position = new Vector3(rigidBody.transform.position.x,
|
|||
|
|
rigidBody.transform.position.y - 0.5f);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Update quest completion.
|
|||
|
|
HashSet<QuestID> quests = (HashSet<QuestID>)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<HurtboxController>();
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|