322 lines
11 KiB
C#
322 lines
11 KiB
C#
#nullable enable
|
|
using UnityEngine;
|
|
using System.Collections;
|
|
using UnityEngine.Serialization;
|
|
|
|
public class AIEntity : Entity {
|
|
|
|
[SerializeField] protected AudioSource attackSource = null!;
|
|
|
|
[SerializeField] protected AudioClip[] attackSounds = null!;
|
|
|
|
[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;
|
|
[field: SerializeField] public float cost{ get; private set; } = 10f;
|
|
|
|
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, bool sound=true) {
|
|
//TODO Should we warn if target is null here?
|
|
if (target != null && target.GetComponent<VampireEntity>() is { })
|
|
target = other.transform;
|
|
|
|
return base.TakeDamage(amount, other, sound);
|
|
}
|
|
|
|
#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<Entity>();
|
|
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<Entity>())) {
|
|
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<Entity>() 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() {
|
|
}
|
|
|
|
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<Entity>();
|
|
if (targetEntity != null) {
|
|
entity.animator.Play("Attack");
|
|
entity.soundManager.PlaySound(entity.attackSource, entity.attackSounds, randomPitch: true, createTempSourceIfBusy: true);
|
|
targetEntity.TakeDamage(entity.attackDmg, entity);
|
|
entity.rb.velocity = Vector3.zero;
|
|
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;
|
|
}
|
|
}
|
|
}
|