#nullable enable using UnityEngine; using System.Collections; using UnityEngine.Serialization; public class AIEntity : Entity { [FormerlySerializedAs("stats")] [SerializeField] public AIStats AIStats = null!; BaseState currentState = null!; public EntityFlag enemies { get; protected set; } public bool facingRight { get; private set; } = true; protected Vector3 moatExtents; protected bool isAvoiding = false; override protected void Start() { base.Start(); currentState = CreateInitialState(); currentState.EnterState(); moatExtents = arena.GetMoatExtents(); //StartCoroutine(StuckCheck()); } override protected void Update() { base.Update(); if (gameFlowManager.pauseLevel >= GameFlowManager.PauseLevel.TimeStop) return; if (currentState.UpdateState() is { } newState) SwitchState(newState); } override protected void FixedUpdate() { base.FixedUpdate(); if (gameFlowManager.pauseLevel >= GameFlowManager.PauseLevel.TimeStop) return; if (currentState.FixedUpdateState() is { } newState) SwitchState(newState); FlipAccordingToInput(); } void OnDrawGizmos() => currentState?.OnDrawGizmos(); protected override void OnDied() { base.OnDied(); transform.SetParent(arena.graveyard); } void SwitchState(BaseState newState) { currentState.LeaveState(); currentState = newState; newState.EnterState(); } IEnumerator StuckCheck(){ yield return new WaitForSeconds(Random.Range(0f,AIStats.stuckCheckTime)); while (true) { if(!isAvoiding) AvoidObstacle(); yield return new WaitForSeconds(AIStats.stuckCheckTime); } } protected virtual BaseState CreateInitialState() => new FindTargetState(this); //Looks into enemy name list to see if the other is targetable virtual protected bool IsTargetable(Entity other) { return enemies.HasFlag(other.entityType) && other.IsAlive(); } override public bool TakeDamage(float amount, Entity other) { //TODO Should we warn if target is null here? if (target != null && target.GetComponent() is { }) target = other.transform; return base.TakeDamage(amount, other); } #region Flip public void Flip() { facingRight = !facingRight; Vector3 scaler = transform.localScale; scaler.x *= -1; transform.localScale = scaler; healthBar.gameObject.transform.localScale = scaler; } public void FlipAccordingToInput() { Vector3 direction = rb.velocity; if (target != null) { direction = target.position - transform.position; } if ((!facingRight && direction.x > 0) || (facingRight && direction.x < 0)) { Flip(); } } #endregion //When ratio is zero, decay is complete. Blood token set to zero public void Decay(float ratio){ if(ratio <= 0){ bloodTokens = 0; OnEmpty(); }else{ Color diffColor = deadColor * ratio; renderer.color = new Color(diffColor.r, diffColor.g, diffColor.b, renderer.color.a); } } public void AvoidObstacle(){ Physics2D.queriesHitTriggers = false; RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, attackRange, (1 << LayerMask.NameToLayer("Safezone")));//Layer 6 is safeZone Physics2D.queriesHitTriggers = true; if(!(hit.collider is null)){ //We have hit the safe zone isAvoiding = true; Vector3 avoidDir = Vector3.zero; //Between top and bottom if(transform.position.y > -moatExtents.y && transform.position.y < moatExtents.y){ avoidDir.y = Mathf.Sign(direction.y) *1.5f; }else if(transform.position.x > -moatExtents.x && transform.position.x < moatExtents.x){//Between left and right avoidDir.x = Mathf.Sign(direction.x) *1.5f; } direction += avoidDir; }else{ isAvoiding = false; } } protected abstract class BaseStateAI : BaseState{ protected AIEntity entity; public BaseStateAI(AIEntity entity){ this.entity = entity; } } protected class SeekState : BaseStateAI { public SeekState(AIEntity entity) : base(entity) { } public override void EnterState() { if (!entity.animator.GetCurrentAnimatorStateInfo(0).IsName("Attack")) { entity.animator.Play("Running"); } } public override BaseState? UpdateState() { if (!entity.IsAlive()) { return new DeadState(entity); } Entity targetEntity = entity.target.GetComponent(); if (targetEntity != null) { if (targetEntity.IsAlive()) {//target is alive, keep chasing it return null; } else {//target is dead, go to findTargetState return new FindTargetState(entity); ; } } return null; } public override BaseState? FixedUpdateState() { entity.direction = Vector3.RotateTowards(entity.direction, (entity.target.position - entity.transform.position), entity.rotSpeed * Time.fixedDeltaTime, 0.0f).normalized; if (entity.IsTargetable(entity.target.GetComponent())) { if (!entity.IsInAttackRange()) { entity.AvoidObstacle(); entity.rb.MovePosition(entity.transform.position + entity.direction * entity.movementSpeed * Time.fixedDeltaTime); // entity.animator.Play("Running"); } else { return new AttackState(entity); } } else { return new FindTargetState(entity); } // entity.animator.Play("Idle"); return null; } } protected class FindTargetState : BaseStateAI { float closeEnough; Vector3 roamPosition; public FindTargetState(AIEntity entity) : base(entity) { } public override void EnterState() { if (!entity.animator.GetCurrentAnimatorStateInfo(0).IsName("Attack")) { entity.animator.Play("Running"); } } public override BaseState? UpdateState() { if (!entity.IsAlive()) { return new DeadState(entity); } Transform entityParent = entity.entityType == EntityFlag.Gladiator ? entity.arena.minionParent : entity.arena.gladiatorParent; float lastDist = float.MaxValue; Transform chosenEntity = null!; foreach (Transform other in entityParent) {// Find the closest entity if (other.GetComponent() is {} otherEntity) { if (entity.IsTargetable(otherEntity)) { float distance = Vector2.Distance(other.position, entity.transform.position); if (distance < lastDist) { lastDist = distance; chosenEntity = other; if (lastDist <= entity.AIStats.closeEnough) break; } } } } if (chosenEntity != null) { entity.target = chosenEntity; return new SeekState(entity); } else { if (roamPosition == new Vector3()) roamPosition = entity.AIStats.getRandomRoamPositon(); return null; } } public override BaseState? FixedUpdateState() { if (roamPosition == new Vector3()) { // entity.animator.Play("Idle"); return null; } entity.direction = Vector3.RotateTowards(entity.direction, (roamPosition - entity.transform.position), entity.rotSpeed * Time.fixedDeltaTime, 0.0f).normalized; if (Vector2.Distance(entity.transform.position, roamPosition) >= entity.attackRange) { entity.AvoidObstacle(); entity.rb.MovePosition(entity.transform.position + entity.direction * entity.movementSpeed * Time.fixedDeltaTime); } else { roamPosition = entity.AIStats.getRandomRoamPositon(); } // entity.animator.Play("Running"); return null; } } protected class AttackState : BaseStateAI { public AttackState(AIEntity entity) : base(entity) { } public override void EnterState() { entity.soundManager.PlaySound(entity.attackSource, entity.attackSounds, randomPitch: true, createTempSourceIfBusy: true); entity.animator.Play("Attack"); } public override BaseState? UpdateState() { if (!entity.IsAlive()) { return new DeadState(entity); } if (entity.gameFlowManager.CanDoAction) { if (entity.IsInAttackRange()) { if (entity.attackTimer >= entity.attackCooldown) { entity.attackTimer = 0; return Attack(); } else { entity.attackTimer += Time.deltaTime; } } else return new SeekState(entity); } return null; } private BaseState? Attack() { Entity targetEntity = entity.target.GetComponent(); if (targetEntity != null) { targetEntity.TakeDamage(entity.attackDmg, entity); bool isTargetAlive = targetEntity.IsAlive(); if (!isTargetAlive) { return new FindTargetState(entity); } } return null; } } protected class DeadState : BaseStateAI{ private float decaytimer; public DeadState(AIEntity entity) : base(entity){ } public override void EnterState() { entity.animator.Play("Death"); decaytimer = 0f; } public override BaseState? UpdateState(){ if(entity.bloodTokens > 0 && !entity.IsBeingSucked()){ decaytimer += Time.deltaTime; entity.Decay(1 - (decaytimer/entity.AIStats.decayTime)); } return null; } } }