505 lines
19 KiB
C#
505 lines
19 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace TBTK{
|
|
|
|
public class AI : MonoBehaviour {
|
|
|
|
#if UNITY_EDITOR
|
|
public static bool inspector=false;
|
|
#endif
|
|
|
|
|
|
public enum _AIBehaviour { passive, aggressive, }// evasive }
|
|
|
|
|
|
[Tooltip("Check to override individual unit behaviour setting")]
|
|
public bool overrideUnitSetting=false;
|
|
|
|
public _AIBehaviour aiBehaviour=_AIBehaviour.aggressive;
|
|
public bool requireTrigger=true; //when true, unit starts in passive state, then switch to aggressive or evasive (doesnt apply for passive)
|
|
|
|
public static bool IsPassive(Unit unit){
|
|
if(!instance.overrideUnitSetting) return unit.IsPassive();
|
|
else return instance.aiBehaviour==AI._AIBehaviour.passive;
|
|
}
|
|
public static bool IsAggressive(Unit unit){
|
|
if(!instance.overrideUnitSetting) return unit.IsAggressive();
|
|
else return (instance.aiBehaviour==AI._AIBehaviour.aggressive && !instance.requireTrigger) || unit.triggered;
|
|
}
|
|
|
|
//public static bool IsPassive(Unit unit){
|
|
//if(!instance.overrideUnitSetting) return unit.IsPassive();
|
|
//else return instance.aiBehaviour==AI._AIBehaviour.passive || !IsAggressive(unit);
|
|
//}
|
|
//public static bool IsAggressive(Unit unit){
|
|
//if(!instance.overrideUnitSetting) return unit.IsPassive();
|
|
//else return instance.aiBehaviour==AI._AIBehaviour.aggressive && (unit.triggered || !instance.requireTrigger);
|
|
//}
|
|
|
|
|
|
[Tooltip("Delay in second between each units when they take their turn")]
|
|
public float delayBetweenUnit=0.25f;
|
|
|
|
[Space(10)] [Tooltip("When checked, AI will always use the best option available")]
|
|
public bool alwaysUseBestOption=true;
|
|
|
|
[Space(10)]
|
|
[Tooltip("How much the damage for a potential attack is going to be weight into making the AI decision ")]
|
|
public float damageMultiplier=1;
|
|
[Tooltip("How much the hit chance for a potential attack is going to be weight into making the AI decision ")]
|
|
public float hitChanceMultiplier=1;
|
|
[Tooltip("How much the critical chance for a potential attack is going to be weight into making the AI decision ")]
|
|
public float critChanceMultiplier=1;
|
|
[Tooltip("How much of giving chase on out of range target is going to be weight into making the AI decision\nOnly applies there are no target within range")]
|
|
public float pursueMultiplier=1;
|
|
[Tooltip("How much cover is going to be weight into making the AI decision\nOnly applies when cover system is enabled ")]
|
|
public float coverMultiplier=1;
|
|
|
|
public static float dmgMul(){ return instance.damageMultiplier; }
|
|
public static float hitMul(){ return instance.hitChanceMultiplier; }
|
|
public static float critMul(){ return instance.critChanceMultiplier; }
|
|
public static float pursueMul(){ return instance.pursueMultiplier; }
|
|
public static float coverMul(){ return instance.coverMultiplier; }
|
|
|
|
|
|
|
|
public static AI instance;
|
|
public static void Init(){
|
|
if(instance==null) instance=(AI)FindObjectOfType(typeof(AI));
|
|
}
|
|
|
|
void Awake(){ instance=this; actionInProgress=false; }
|
|
|
|
|
|
[Space(10)] private static bool actionInProgress=false;
|
|
public static bool ActionInProgress(){ return actionInProgress; }
|
|
|
|
|
|
public static void MoveUnit(Unit unit){ instance.StartCoroutine(_MoveUnit(unit)); }
|
|
public static IEnumerator _MoveUnit(Unit unit){
|
|
actionInProgress=true;
|
|
|
|
TBTK.OnGameMessage("- AI's Turn -");
|
|
TBTK.OnSelectUnit(unit);
|
|
|
|
yield return instance.StartCoroutine(AIRoutineUnit(unit));
|
|
|
|
if(unit==null || unit.hp<=0) yield return new WaitForSeconds(0.25f);
|
|
if(instance.delayBetweenUnit>0) yield return new WaitForSeconds(instance.delayBetweenUnit);
|
|
|
|
actionInProgress=false;
|
|
|
|
GameControl.EndTurn();
|
|
}
|
|
|
|
public static void MoveFaction(Faction faction){ instance.StartCoroutine(_MoveFaction(faction)); }
|
|
public static IEnumerator _MoveFaction(Faction faction){
|
|
actionInProgress=true;
|
|
|
|
TBTK.OnGameMessage("- AI's Turn -");
|
|
|
|
List<Unit> unitList=new List<Unit>( faction.unitList );
|
|
|
|
if(TurnControl.EnableUnitLimit() || Rand.value()<0.4f){
|
|
List<Unit> newList=new List<Unit>();
|
|
while(unitList.Count>0){
|
|
int rand=Rand.Range(0, unitList.Count);
|
|
newList.Add(unitList[rand]);
|
|
unitList.RemoveAt(rand);
|
|
}
|
|
unitList=newList;
|
|
}
|
|
|
|
if(unitList.Count>TurnControl.GetUnitLimit()){
|
|
while(unitList.Count>TurnControl.GetUnitLimit()) unitList.RemoveAt(unitList.Count-1);
|
|
}
|
|
|
|
for(int i=0; i<unitList.Count; i++){
|
|
if(unitList[i]==null) continue;
|
|
|
|
Unit unit=unitList[i]; TBTK.OnSelectUnit(unit);
|
|
yield return instance.StartCoroutine(AIRoutineUnit(unit));
|
|
|
|
if(unit==null || (unit.hp<=0 && unit.IsVisible())){ yield return new WaitForSeconds(0.25f); }
|
|
|
|
if(unit!=null && instance.delayBetweenUnit>0 && unit.IsVisible()) yield return new WaitForSeconds(instance.delayBetweenUnit);
|
|
}
|
|
|
|
actionInProgress=false;
|
|
|
|
GameControl.EndTurn();
|
|
}
|
|
|
|
public static IEnumerator AIRoutineUnit(Unit unit){
|
|
if(unit.IsStunned()) yield break;
|
|
|
|
int safetyCounter=0;
|
|
while(unit.CanMove() || unit.CanAttack()){
|
|
AIAction action=AnalyseAction(unit);
|
|
|
|
if(action==null){
|
|
//Debug.Log("No valid action for unit - "+unit.gameObject);
|
|
yield break;
|
|
}
|
|
|
|
if(action.tgtNode==unit.node && action.tgtUnit==null){
|
|
safetyCounter+=1;
|
|
if(safetyCounter>3) yield break;
|
|
else continue;
|
|
}
|
|
|
|
if(action.tgtNode!=unit.node){
|
|
yield return instance.StartCoroutine(unit.MoveRoutine(action.tgtNode));
|
|
}
|
|
|
|
if(unit.hp<=0) yield break;
|
|
|
|
if(unit!=null && action.tgtUnit!=null){
|
|
yield return instance.StartCoroutine(unit.AttackRoutine(action.tgtUnit.node));
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
public static AIAction AnalyseAction(Unit unit){
|
|
//Debug.Log("AnalyseAction "+unit.gameObject);
|
|
|
|
if(IsPassive(unit) && Rand.value()<0.7f) return new AIAction(unit.node);
|
|
|
|
List<AIAction> actionList=new List<AIAction>();
|
|
|
|
//List<Unit> hostileList=new List<Unit>();
|
|
bool nearestHostileScanned=false;
|
|
Node nearestHostileNode=null; float nearestHostileDist=Mathf.Infinity;
|
|
//Node furthestHostileNode=null; float furthestHostileDist=0;
|
|
float maxNearestDistToHostile=0; float minNearestDistToHostile=Mathf.Infinity;
|
|
bool hasTargetWithinRange=false;
|
|
|
|
float unitDamage=-99;
|
|
|
|
List<Node> walkableList=unit.CanMove() ? GridManager.SetupWalkableList(unit) : null;
|
|
if(walkableList==null) walkableList=new List<Node>();
|
|
walkableList.Insert(0, unit.node);
|
|
|
|
List<Unit> allhostileList=null;
|
|
if(GameControl.EnableCoverSystem()) allhostileList=UnitManager.GetAllHostileUnits(unit.GetFacID());
|
|
|
|
|
|
for(int i=0; i<walkableList.Count; i++){
|
|
List<Node> attackNodeList=GridManager.GetAttackableList(unit, walkableList[i]);
|
|
|
|
Vector2 cover=CheckCover(walkableList[i], unit, allhostileList);
|
|
float coverScore=100 * (cover[0]!=0 ? cover[0] : cover[1]) * 0.5f ; //bcz full cover value is 2
|
|
|
|
if(attackNodeList.Count>0){
|
|
if(unitDamage<=-99) unitDamage=(unit.GetDmgHPMin() + unit.GetDmgHPMax()) * 0.5f;
|
|
|
|
for(int n=0; n<attackNodeList.Count; n++){
|
|
AIAction action=new AIAction(walkableList[i], attackNodeList[n].unit);
|
|
|
|
Attack attack=new Attack(unit, attackNodeList[n].unit, walkableList[i], false, false);
|
|
action.score+=(0.5f*(attack.damageHPMin+attack.damageHPMax))/unitDamage*100*instance.damageMultiplier;
|
|
action.score+=attack.hitChance*100f*instance.hitChanceMultiplier;
|
|
action.score+=attack.critChance*100f*instance.critChanceMultiplier;
|
|
action.score+=coverScore;
|
|
|
|
//~ float score1=(0.5f*(attack.damageHPMin+attack.damageHPMax))/unitDamage*100*instance.damageMultiplier;
|
|
//~ float score2=attack.hitChance*100f*instance.hitChanceMultiplier;
|
|
//~ float score3=attack.critChance*100f*instance.critChanceMultiplier;
|
|
//~ float score4=coverScore;
|
|
//~ float ccover=Attack.GetCover(walkableList[i], attackNodeList[n]);
|
|
|
|
//~ Debug.Log((0.5f*(attack.damageHPMin+attack.damageHPMax))+" "+unitDamage+" "+attack.cover+" "+score1+" "+score2+" "+score3+" "+score4+" ");
|
|
|
|
//Debug.Log(coverScore+" "+attack.cover+" "+attack.hitChance+" "+attack.critChance+" "+action.score);
|
|
//Debug.DrawLine(walkableList[i].GetPos(), walkableList[i].GetPos()+new Vector3(0, 1, 0)*(action.score*0.01f), Color.white, 2);
|
|
|
|
actionList.Add(action);
|
|
}
|
|
|
|
hasTargetWithinRange=true;
|
|
}
|
|
else{
|
|
|
|
AIAction action=new AIAction(walkableList[i], coverScore);
|
|
|
|
if(IsPassive(unit)){
|
|
action.score+=Mathf.Min(0, 3-GridManager.GetDistance(walkableList[i], unit.node));
|
|
}
|
|
else if(IsAggressive(unit)){
|
|
//Node nearestHostileNode=null; float nearestHostileDist=0;
|
|
|
|
if(!nearestHostileScanned){
|
|
nearestHostileScanned=true;
|
|
|
|
if(allhostileList==null) allhostileList=UnitManager.GetAllHostileUnits(unit.GetFacID());
|
|
|
|
if(allhostileList.Count>0){
|
|
int nearestIdx=0; //float nearest=Mathf.Infinity;
|
|
//int furthestIdx=0; //float furthest=0;
|
|
for(int n=0; n<allhostileList.Count; n++){
|
|
float dist=GridManager.GetDistance(unit.node, allhostileList[n].node);
|
|
if(dist<nearestHostileDist){ nearestHostileDist=dist; nearestIdx=n; }
|
|
//if(dist>furthest){ furthestHostileDist=dist; furthestIdx=n; }
|
|
}
|
|
nearestHostileNode=allhostileList[nearestIdx].node;
|
|
//furthestHostileNode=hostileList[furthestIdx].node;
|
|
}
|
|
}
|
|
|
|
//if(nearestHostileNode==null){
|
|
//List<Unit> hostileList=UnitManager.GetAllHostileUnits(unit.GetFacID());
|
|
//~ int nearestIdx=0; float nearest=Mathf.Infinity;
|
|
//~ int furthestIdx=0; float furthest=0;
|
|
//~ for(int n=0; n<hostileList.Count; n++){
|
|
//~ float dist=GridManager.GetDistance(unit.node, hostileList[n].node);
|
|
//~ if(dist<nearest){ nearest=dist; nearestIdx=n; }
|
|
//~ if(dist>furthest){ furthest=dist; furthestIdx=n; }
|
|
//~ }
|
|
//nearestHostileNode=hostileList[nearestIdx].node;
|
|
//nearestHostileDist=nearest;
|
|
//}
|
|
|
|
if(nearestHostileNode!=null){
|
|
action.scoreAlt=GridManager.GetDistance(walkableList[i], nearestHostileNode);
|
|
//float nearestDistToHostile=GridManager.GetDistance(walkableList[i], nearestHostileNode);
|
|
//action.scoreAlt=Mathf.Max(0, nearestHostileDist-nearestDistToHostile);// * instance.pursueMultiplier;
|
|
//action.scoreAlt*=Rand.Range(0.75f, 1.25f);
|
|
|
|
//Debug.Log(nearestHostileDist+" "+nearestDistToHostile+" "+action.scoreAlt+" "+action.score);
|
|
//Debug.Log(action.scoreAlt+" "+maxNearestDistToHostile);
|
|
|
|
if(action.scoreAlt<minNearestDistToHostile) minNearestDistToHostile=action.scoreAlt;
|
|
if(action.scoreAlt>maxNearestDistToHostile) maxNearestDistToHostile=action.scoreAlt;
|
|
//if(action.scoreAlt>maxNearestDistToHostile) maxNearestDistToHostile=nearestDistToHostile;
|
|
}
|
|
|
|
//~ if(nearest==furthest) action.score+=1;
|
|
//~ else action.score+=Mathf.Abs(nearest-furthest)/(furthest-nearest) * instance.pursueMultiplier;
|
|
|
|
//float distToNearestHostile=GridManager.GetDistance(walkableList[i], nearestHostileNode);
|
|
//
|
|
//action.score+=Mathf.Max(0, nearestHostileDist-distToNearestHostile)*instance.pursueMultiplier;
|
|
}
|
|
|
|
actionList.Add(action);
|
|
}
|
|
}
|
|
|
|
if(IsAggressive(unit)){
|
|
for(int i=0; i<actionList.Count; i++){
|
|
if(!actionList[i].CachedScore()) continue;
|
|
|
|
float range=maxNearestDistToHostile-minNearestDistToHostile;
|
|
actionList[i].scoreAlt=1-(actionList[i].scoreAlt-minNearestDistToHostile)/range;
|
|
|
|
//Debug.Log(i+" alt score - "+actionList[i].scoreAlt+" "+actionList[i].score);
|
|
//actionList[i].scoreAlt=(maxNearestDistToHostile-actionList[i].scoreAlt);///maxNearestDistToHostile * instance.pursueMultiplier;
|
|
//Debug.Log(" - "+actionList[i].scoreAlt+" "+maxNearestDistToHostile);
|
|
//~ actionList[i].score+=(100*actionList[i].scoreAlt/maxNearestDistToHostile) * instance.pursueMultiplier;
|
|
|
|
actionList[i].score+=100 * actionList[i].scoreAlt * instance.pursueMultiplier;
|
|
}
|
|
}
|
|
|
|
//no target in range but unit has attacked
|
|
//unit has either just destroyed a unit within range or use up or it's attack option
|
|
if(!hasTargetWithinRange && unit.attackThisTurn<=0){
|
|
if(!unit.CanMove()) return null;
|
|
|
|
if(IsAggressive(unit) && !GameControl.EnableCoverSystem()){
|
|
//List<Node> path=AStar.SearchWalkableNode(unit.node, nearestHostileNode, unit.canMovePastUnit, unit.canMovePastObs, true);
|
|
List<Node> path=AStar.SearchWalkableNode(unit.node, nearestHostileNode, AStar.BypassUnitCode(unit), unit.canMovePastObs, true);
|
|
if(path.Count>0){
|
|
int idx=Mathf.Clamp(unit.GetMoveRange()-1, 0, path.Count-1);
|
|
AIAction action=new AIAction(path[idx], 0);
|
|
return action;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(actionList.Count==0) return null;
|
|
|
|
//sorting the list according to the score
|
|
List<AIAction> newList=new List<AIAction>();
|
|
while(actionList.Count>0){
|
|
int highestIdx=-1; float highest=-Mathf.Infinity;
|
|
for(int i=0; i<actionList.Count; i++){
|
|
if(actionList[i].score>highest){ highest=actionList[i].score; highestIdx=i; }
|
|
}
|
|
|
|
if(highestIdx<0) break;
|
|
|
|
if(instance.alwaysUseBestOption) return actionList[highestIdx];
|
|
|
|
newList.Add(actionList[highestIdx]);
|
|
actionList.RemoveAt(highestIdx);
|
|
}
|
|
actionList=newList;
|
|
|
|
if(actionList[0].score>0){
|
|
for(int i=1; i<actionList.Count; i++){
|
|
if(actionList[i].score>0) continue;
|
|
actionList.RemoveAt(i); i-=1;
|
|
}
|
|
}
|
|
|
|
if(actionList.Count==0) return null;
|
|
|
|
int rand=Rand.GetOption(new List<float>{ 1.5f, 0.25f, 0.125f, 0.075f });
|
|
rand=Mathf.Min(rand, actionList.Count-1);
|
|
|
|
return actionList[rand];
|
|
}
|
|
/*
|
|
public static AIAction __AnalyseAction(Unit unit){
|
|
List<AIAction> actionList=new List<AIAction>();
|
|
|
|
List<Node> walkableList=GridManager.SetupWalkableList(unit);
|
|
walkableList.Insert(0, unit.node);
|
|
|
|
for(int i=0; i<walkableList.Count; i++){
|
|
List<Node> attackNodeList=GridManager.GetAttackableList(unit, walkableList[i]);
|
|
//
|
|
|
|
float coverScore=100 * CheckCover(walkableList[i], unit) * instance.coverMultiplier;
|
|
|
|
if(attackNodeList.Count>0){
|
|
for(int n=0; n<attackNodeList.Count; n++){
|
|
|
|
AIAction action=new AIAction(walkableList[i], attackNodeList[n].unit);
|
|
|
|
Attack attack=new Attack(unit, attackNodeList[n].unit, false, false);
|
|
action.score+=0.5f*(attack.damageHPMin+attack.damageHPMax)*instance.damageMultiplier;
|
|
action.score+=attack.hitChance*100*instance.hitChanceMultiplier;
|
|
action.score+=attack.critChance*100*instance.critChanceMultiplier;
|
|
action.score+=coverScore;
|
|
|
|
actionList.Add(action);
|
|
}
|
|
}
|
|
else{
|
|
|
|
}
|
|
}
|
|
|
|
if(actionList.Count==0){
|
|
if(unit.IsPassive()){
|
|
if(Rand.value()<0.7f) return new AIAction(unit.node);
|
|
else{
|
|
for(int i=1; i<walkableList.Count; i++){
|
|
float coverScore=100 * CheckCover(walkableList[i], unit) * instance.coverMultiplier;
|
|
float dist=GridManager.GetDistance(unit.node, walkableList[i]);
|
|
if(dist==1) actionList.Add(new AIAction(walkableList[i], coverScore));
|
|
else if(dist==2 && Rand.value()<0.3f) actionList.Add(new AIAction(walkableList[i], coverScore));
|
|
}
|
|
|
|
//if unit is not triggered or cover system is not active,
|
|
//just move randomly, otherwise let the sorting algorithm at the end of the function to choose the node with higeste cover
|
|
if(actionList.Count>0 && (!unit.triggered || !GameControl.EnableCoverSystem()))
|
|
return actionList[Rand.Range(0, actionList.Count)];
|
|
}
|
|
}
|
|
else if(unit.IsAggressive()){
|
|
float furthestDist=0;
|
|
List<Unit> hostileList=UnitManager.GetAllHostileUnits(unit.facID);
|
|
|
|
for(int i=0; i<walkableList.Count; i++){
|
|
float nearest=Mathf.Infinity;
|
|
for(int n=0; n<hostileList.Count; n++){
|
|
float dist=GridManager.GetDistance(walkableList[i], hostileList[n].node);
|
|
if(dist>furthestDist) furthestDist=dist;
|
|
if(dist<nearest) nearest=dist;
|
|
}
|
|
|
|
actionList.Add(new AIAction(walkableList[i], nearest, 100*CheckCover(walkableList[i], unit)*instance.coverMultiplier));
|
|
}
|
|
|
|
for(int i=0; i<actionList.Count; i++){
|
|
actionList[i].score=instance.pursueMultiplier * (furthestDist-actionList[i].score)/furthestDist;
|
|
actionList[i].score+=actionList[i].scoreAlt;
|
|
}
|
|
}
|
|
}
|
|
|
|
int highestIdx=-1; float highest=-1;
|
|
for(int i=0; i<actionList.Count; i++){
|
|
if(actionList[i].score>highest){
|
|
highest=actionList[i].score; highestIdx=i;
|
|
}
|
|
}
|
|
|
|
//Debug.Log("AI action: "+actionList.Count+" "+highestIdx);
|
|
|
|
if(actionList.Count>0){
|
|
if(highestIdx>=0) return actionList[highestIdx];
|
|
else return actionList[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
*/
|
|
|
|
public static Vector2 CheckCover(Node node, Unit unit, List<Unit> hostileList){
|
|
if(!GameControl.EnableCoverSystem()) return Vector2.zero;
|
|
|
|
//consider xcom style side stepping for square grid
|
|
|
|
int lowestCover=2; float totalCover=0;
|
|
|
|
//List<Unit> hostileList=UnitManager.GetAllHostileUnits(unit.GetFacID());
|
|
for(int i=0; i<hostileList.Count; i++){
|
|
//~ if(GridManager.GetDistance(hostileList[i].node, node)>hostileList[i].GetAttackRange()) continue;
|
|
//~ //Vector3 dir=hostileList[i].node.GetPos()-node.GetPos();
|
|
//~ //int cover=node.GetCover(Utility.Vector2ToAngle(new Vector2(dir.x, dir.z)));
|
|
//~ int cover=node.GetCover(GridManager.GetAngle(node, hostileList[i].node, false));
|
|
//~ if(cover<lowestCover) lowestCover=cover;
|
|
|
|
int cover=Attack.GetCover(hostileList[i].node, node);
|
|
if(cover<lowestCover) lowestCover=cover;
|
|
|
|
totalCover+=cover;
|
|
}
|
|
|
|
totalCover=totalCover/(float)hostileList.Count * 0.4f;
|
|
|
|
//Color color=lowestCover==0 ? Color.red : Color.white ;
|
|
//Debug.DrawLine(node.GetPos(), node.GetPos()+new Vector3(0, 1, 0)*(totalCover), color, 2);
|
|
|
|
return new Vector2(lowestCover, totalCover);
|
|
|
|
//~ Vector3 avgPos=Vector3.zero;
|
|
//~ List<Unit> hostileList=UnitManager.GetAllHostileUnits(unit.facID);
|
|
//~ for(int n=0; n<hostileList.Count; n++) avgPos+=hostileList[n].GetPos();
|
|
//~ avgPos/=hostileList.Count;
|
|
|
|
//~ Vector3 dir=avgPos-node.GetPos();
|
|
//~ return node.GetCover(Utility.Vector2ToAngle(new Vector2(dir.x, dir.z)));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public class AIAction{
|
|
public Node tgtNode;
|
|
public Unit tgtUnit;
|
|
public float score=0;
|
|
public float scoreAlt=-999; //used to temporarily cache value
|
|
|
|
public AIAction(Node node, Unit tgtU=null){
|
|
tgtNode=node; tgtUnit=tgtU;
|
|
}
|
|
public AIAction(Node node, float ss, float ssAlt=0){
|
|
tgtNode=node; score=ss; scoreAlt=ssAlt;
|
|
}
|
|
|
|
public bool CachedScore(){ return scoreAlt!=-999; }
|
|
}
|
|
|
|
} |