Files
pgs/Assets/Scripts/MonsterController.cs
2026-02-21 16:58:22 -08:00

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);
}
}