/******************************************************************
  
  Module:  aigoal.cpp
  
  Author: Borut Pfeifer
  
  Copyright 2005 Sony Online Entertainment.  All rights reserved.
  
*******************************************************************/

//-------------------------------------------------------- Includes
#include "aigoal.h"
#include "aigoal_attack.h"
#include "aigoal_noncombat.h"
#include "aigoal_cast.h"
#include "../intel.h"
#include "../stats.h"
#include "../registry.h"
#include "../graphic.h"
#include "../physics.h"
#include "../titan.h"
#include "../tuning.h"
#include "../database.h"
#include "../cameracontroller.h"
#include "behaviortypes.h"
#include "lostable.h"
#include "aidebugdraw.h"
#include "aiblackboard.h"
#include "sycamera.h"

//------------------------------------ cGoalQueue
cGoalQueue::cGoalQueue()
: mpOwner(NULL),
  mpCurGoal(NULL)
{
}

cGoalQueue::~cGoalQueue()
{
  for (int ii=mGoals.Begin();ii!=mGoals.End();ii=mGoals.Next(ii))
  {
    delete mGoals(ii);
  }

  mGoals.Clear();  
}

void cGoalQueue::Init(cAi* pOwner)
{
  SyAssert(pOwner != NULL);

  mpOwner = pOwner;
}

int cGoalQueue::GetHighestPriority()
{
  if (mGoals.Size() > 0)
  {
    // Enter new goal if it was started
    return mGoals(mGoals.Prev(mGoals.End()))->GetPriority();
  }

  return -1;
}

void cGoalQueue::Push(int priority, cAiGoal *pGoal)
{
  int i;
  int end = mGoals.Add();

  pGoal->Init(mpOwner, priority);

  SyAssertf(priority >= 0, "cGoalQueue::Push can't add a goal with a negative priority!");

  for (i=mGoals.Begin();i!=end;i=mGoals.Next(i))
  {
    if (mGoals(i)->GetPriority() > priority)
    {
      break;
    }
  }

  Insert(pGoal, i);
}


void cGoalQueue::Pop(cAiGoal *pGoal)
{
  // setting the priority to negative marks it to be deleted
  pGoal->SetPriority(-1);
}

void cGoalQueue::Clear()
{
  int i;
  int end = mGoals.End();

  for (i=mGoals.Begin();i!=end;i=mGoals.Next(i))
  {
    mGoals(i)->SetPriority(-1);
  }
}

void cGoalQueue::Remove(cAiGoal *pGoal)
{
  for (int i=0;i!=mGoals.End();i=mGoals.Next(i))
  {
    if (mGoals(i) == pGoal)
    {
      for (;mGoals.Next(i) != mGoals.End();i=mGoals.Next(i))
      {
        mGoals(i) = mGoals(mGoals.Next(i));
      }
      mGoals.Erase();
      return;
    }
  }

  SyAssertf(false, "Goal not found");
}

void cGoalQueue::Insert(cAiGoal *pGoal, int index)
{
  int i=mGoals.End();
  i=mGoals.Prev(i);

  for (;i!=index;i=mGoals.Prev(i))
  {
    mGoals(i) = mGoals(mGoals.Prev(i));
  }

  mGoals(index) = pGoal;
}

void cGoalQueue::ChangePriority(cAiGoal *pGoal, int priority)
{
  SyAssert(pGoal);

  if (!pGoal)
  {
    return;
  }

  if (pGoal->GetPriority() != priority)
  {
    pGoal->SetPriority(priority);
    Remove(pGoal);
    Push(priority, pGoal);
  }
}

cAiGoal* cGoalQueue::FindGoalType(eGoalType type)
{
  cAiGoal *pGoal;
  for (int i=mGoals.Begin();i!=mGoals.End();i=mGoals.Next(i))
  {
    pGoal = mGoals(i);
    if (pGoal->GetType() == type) 
    {
      return pGoal;
    }
  }

  return NULL;
}

void cGoalQueue::Update(float time)
{
  SyAssertf(mGoals.Size() < 32, "Whoa, npc AI has too many goals at once.");

  if (mGoals.Size() > 0)
  {
    // Enter new goal if it was started
    if (mpCurGoal != mGoals(mGoals.Prev(mGoals.End())))
    {
      mpCurGoal = mGoals(mGoals.Prev(mGoals.End()));
      mpCurGoal->Enter();
    }

    // let current goal run
    SyAssert(mpCurGoal);
    mpCurGoal->Update(time);

    // now check for popped goals
    cAiGoal* pGoal;
    int i = 0;

    while (i < mGoals.Size())
    {
      pGoal = mGoals(i);

      if (pGoal->GetPriority() < 0)  // was marked to be deleted
      {
        if (mpCurGoal == pGoal)
        {
          mpCurGoal->Exit();
          mpCurGoal = NULL;
        }

        for (int j=i; mGoals.Next(j) != mGoals.End(); j=mGoals.Next(j))
        {
          mGoals(j) = mGoals(mGoals.Next(j));
        }

        mGoals.Erase();
        delete pGoal;
      }
      else
      {
        ++i;
      }
    }
  }
}
//------------------------------------ cAiInterface

bool cAiInterface::sbDisabled = false;

cAiInterface:: ~cAiInterface()
{
}

cAiInterface *cAiInterface::Allocate(cGameObject *owner)
{
  cAi* new_ai = new cAi;
  new_ai->Init(owner);
  return new_ai;
}

void cAiInterface::DisableAll(bool bDisable)
{
  sbDisabled = bDisable;
}

cAiInterface::cAiInterface() 
{
}

//------------------------------------ cAi

float cAi::smDefendRadius = 15.0f;
float cAi::smActivationRadius = 15.0f;


cAi::cAi() :
  mpOwner(NULL),
  mAttackTarget(),
  mDefendTarget(),
  mLastDodgedProjectileID(ID_NONE)
{
}

cAi::~cAi()
{
  // this stuff should've been cleaned up by a reset call elsewhere
  SyAssert(cAIBlackboard::Get()->CountRecords(cAIBlackboard::BBR_INVALID, mpOwner->GetID()) == 0);
  mAttackTarget.Clear();
  mDefendTarget.Clear();

  mpOwner = NULL;
}

void cAi::Init(cGameObject *pOwner)
{
  mpOwner = pOwner;
  mGoalQueue.Init(this);

  cAiGoal* pGoal = NULL;
  char behType = static_cast<cStatsCharacter*>(mpOwner->GetStats())->GetBehaviorType();

  switch (behType)
  {
  case NPCBEH_MAXTYPES:
    SyAssertf(false, "Bad Behavior Type in cAI::AttackTarget");
    break;

  case NPCBEH_NONE:
  case NPCBEH_RANGEDONLY:
  case NPCBEH_MELEERANGED: 
  case NPCBEH_RANGEDLINE:
  case NPCBEH_RANGEDCIRCLE:
  case NPCBEH_RANGEDSTUN:
  case NPCBEH_RANGEDTELEPORT:
  case NPCBEH_MELEESWARM:
  case NPCBEH_MELEEFLANK:
  case NPCBEH_MELEECHARGERETREAT:
  case NPCBEH_MELEEENRAGE:
  case NPCBEH_MELEESTEALTH:
  case NPCBEH_MELEESTUN:
  case NPCBEH_MELEEWANDER:
  case NPCBEH_KAMIKAZE:
  case NPCBEH_MELEEONLY:
  default:
    break;

  case NPCBEH_RUNNER:
    pGoal = new cGoal_Flee(ID_NONE, 0.0f, 10.0f);
    break;

  case NPCBEH_CAST_SUMMON:
    pGoal = new cGoal_CastSummon();
    break;

  case NPCBEH_CAST_HEAL:
    pGoal = new cGoal_CastHeal();
    break;

  case NPCBEH_CAST_EXPLOSION:
    pGoal = new cGoal_CastExplosion();
    break;
  }

  if (pGoal)
  {
    mGoalQueue.Push(1, pGoal);
  }
}

bool cAi::IsOutsideDefendRadius()
{
  cGameObject* pDefendTarget = GetDefendTarget();

  if (pDefendTarget != NULL && 
      pDefendTarget->GetLocation().DistanceSquared(mpOwner->GetLocation())
      > (smDefendRadius * smDefendRadius))
  {
    return true;
  }

  return false;
}


void cAi::Update(float time)
{
  if (sbDisabled)
  {
    prop_cast<cIntelNPC*>(mpOwner->GetIntel())->Stop();
    SetAttackTarget(ID_NONE);
    return;
  }

  FindTarget();

  mAttackTarget.Update(mpOwner, time);
  mDefendTarget.Update(mpOwner, time);

  CheckForIncomingProjectiles();
  CheckForNegativeAreaEffects();

  mGoalQueue.Update(time);
}

void cAi::FindTarget()
{
  if (mpOwner->GetStats()->IsDead())
  {
    return;
  }

  // only try to find targets if we're not neutral
  if (static_cast<cStatsCharacter*>(mpOwner->GetStats())->GetFaction() == NPCFACTION_NEUTRAL)
  {
    return;
  }

  if (IsOutsideDefendRadius())
  {
    // no new targets, must get inside defend radius
    return;
  }

  float testRadius = GetActivationRadius();

  if (IsOnCamera())
  {
    testRadius = SY_MIN(40.0f, testRadius*1.25f);
  }

  // find targetable enemies
  cGameObject* pClosestObj = static_cast<cIntelNPC*>(mpOwner->GetIntel())->FindClosestObject(0.0f, testRadius, true, true, false, false);
  SyVect3 myLoc(mpOwner->GetLocation());

  if (!pClosestObj)
  {
    // test nearby melee attackers if they need help
    static const SyTime CALLFORHELP_DELAY = SyTime(2000);
    cAIBlackboard::Record rec;
    bool bCallForHelp = false;
    cGameObject* pAttacker;
    float callForHelpRadiusSqr = (GetActivationRadius()*0.5f)*(GetActivationRadius()*0.5f);
    SyTime curTime = SyTimer::GetTime();

    if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_ATTACKING_MELEE, ID_NONE, ID_NONE, rec))
    {
      do
      {
        if ((curTime - rec.mCreatedTimestamp > CALLFORHELP_DELAY) &&
            (myLoc.DistanceSquared(rec.mLocation) < callForHelpRadiusSqr))
        {
          pAttacker = mpOwner->GetRegistry()->Fetch(rec.mSourceID);
          if (pAttacker && mpOwner->GetIntel()->IsFriendly(pAttacker))
          {
            bCallForHelp = true;
            pClosestObj = mpOwner->GetRegistry()->Fetch(rec.mTargetID);
          }
        }
      }
      while (!bCallForHelp && cAIBlackboard::Get()->GetNextRecord(rec));
    }

    if (!bCallForHelp && cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_ATTACKING_RANGED, ID_NONE, ID_NONE, rec))
    {
      do
      {
        if ((curTime - rec.mCreatedTimestamp > CALLFORHELP_DELAY) &&
            (myLoc.DistanceSquared(rec.mLocation) < callForHelpRadiusSqr))
        {
          pAttacker = mpOwner->GetRegistry()->Fetch(rec.mSourceID);
          if (pAttacker && mpOwner->GetIntel()->IsFriendly(pAttacker))
          {
            bCallForHelp = true;
            pClosestObj = mpOwner->GetRegistry()->Fetch(rec.mTargetID);
          }
        }
      }
      while (!bCallForHelp && cAIBlackboard::Get()->GetNextRecord(rec));
    }
  }

  if (pClosestObj && 
      (GetAttackTarget() != pClosestObj || !mGoalQueue.FindGoalType(GOAL_ATTACK)))
  {
    float distSqr = myLoc.DistanceSquared(pClosestObj->GetLocation());
    SyVect3 oldTargetLoc;
    bool bHasTargetPos = GetLastSeenAttackTargetPos(oldTargetLoc);

    // attack closest target if we have no prev target or 
    // it's more than half the distance closer
    if (GetAttackTarget() == pClosestObj ||
        !bHasTargetPos ||
        myLoc.DistanceSquared(oldTargetLoc) > (distSqr+(GetActivationRadius()*GetActivationRadius()*0.25f)))
    { 
      if (!GetDefendTarget() ||
          !IsMeleeAttacker() ||
          GetDefendTarget()->GetDistance(pClosestObj) < GetDefendRadius())
      {
        SetAttackTarget(pClosestObj->GetID());
        AttackTarget(10); // then attack
        return;
      }
    }
  }

  // no target
  if (static_cast<cIntelNPC*>(mpOwner->GetIntel())->GetStartLocation().Distance(myLoc) > 5.0f)
  {
    GoTo(0, static_cast<cIntelNPC*>(mpOwner->GetIntel())->GetStartLocation());
  }
}

void cAi::SetAttackTarget(tGameObjectID targetID)
{
  mAttackTarget.SetTarget(targetID);
}

cGameObject* cAi::GetAttackTarget()
{
  return mAttackTarget.GetTarget(mpOwner);
}

bool cAi::CanSeeAttackTarget()
{
  return mAttackTarget.CanSeeTarget();
}

bool cAi::HasLOSToAttackTarget()
{
  return mAttackTarget.HasLOS();
}

bool cAi::GetLastSeenAttackTargetPos(SyVect3& pos)
{
  return mAttackTarget.GetLastSeenTargetPos(pos);
}


void cAi::SetDefendTarget(tGameObjectID targetID)
{
  mDefendTarget.SetTarget(targetID);

  SyAssertf(!mDefendTarget.GetTarget(mpOwner) || static_cast<cIntelNPC*>(mpOwner->GetIntel())->IsFriendly(mDefendTarget.GetTarget(mpOwner)), "Follow target is not a friendly NPC type");
}

cGameObject* cAi::GetDefendTarget()
{
  return mDefendTarget.GetTarget(mpOwner);
}

bool cAi::CanSeeDefendTarget()
{
  return mDefendTarget.CanSeeTarget();
}

bool cAi::GetLastSeenDefendTargetPos(SyVect3& pos)
{
  return mDefendTarget.GetLastSeenTargetPos(pos);
}


void cAi::Pop(cAiGoal *goal)
{
  mGoalQueue.Pop(goal);
}

void cAi::ChangePriority(cAiGoal *pGoal, int priority)
{
  mGoalQueue.ChangePriority(pGoal, priority);
}

void cAi::Reset()
{
  mGoalQueue.Clear();
  mAttackTarget.CancelLOSRequest();
  mDefendTarget.CancelLOSRequest();
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_INVALID, mpOwner->GetID());
}


void cAi::CheckForIncomingProjectiles()
{
  static const float INCOMING_PROJECTILE_DODGE_DOT = 0.98f;
  static const float INCOMING_PROJECTILE_DODGE_REACT_TIME = 0.70f;
  static const float INCOMING_PROJECTILE_FOV_DOT = 0.1f;
  static const float INCOMING_PROJECTILE_MAX_HEIGHT_DELTA = 10.0f;
  static const float INCOMING_PROJECTILE_FROMBEHIND_NEAR = 3.0f;

  cGameObjectRegistry* pRegistry = mpOwner->GetRegistry();

  if (!pRegistry)
  {
    return;
  }

  int endReg = pRegistry->End();
  cGameObject* pProjectile;

  bool bProjFromBehind = false;
  SyVect3 toProj, projDir, dodge, fromBehindDir;
  float distToProj, dotProjDir;
  SyVect3 myLoc(mpOwner->GetLocation()), myDir;
  myDir.HPR(mpOwner->GetHeading(), 0.0f, 0.0f);

  for (int i=pRegistry->Begin(); i!=endReg; i=pRegistry->Next(i))
  {
    pProjectile = (*pRegistry)(i);

    if (!pProjectile ||
        pProjectile->GetType() != cGameObject::OBJ_PROJECTILE ||
        pProjectile->GetStats()->IsDead() ||
        pProjectile->GetID() == mLastDodgedProjectileID)
    {
      continue;
    }

    toProj = pProjectile->GetLocation() - myLoc;

    SyAssertf(pProjectile->GetStats()->DynamicCast(STATSPROJECTILE_CLASSID) != NULL, "Bad obj type or stats type in cAi::CheckForIncomingProjectiles: %d", pProjectile->GetID());
    SyAssertf(static_cast<cStatsProjectile*>(pProjectile->GetStats())->GetMaster() != NULL, "Projectile has no master stats in cAi::CheckForIncomingProjectiles: %d", pProjectile->GetID());

    if (static_cast<cStatsProjectile*>(pProjectile->GetStats())->GetMaster()->mGravity > 0.0f)
    {
      // don't consider elevation of projectile if it's falling on us
      toProj.Y = 0;
    }
    else if (toProj.Y > INCOMING_PROJECTILE_MAX_HEIGHT_DELTA)
    {
      continue;
    }

    distToProj = toProj.NormalizeMagn();
    projDir = pProjectile->GetPhysics()->GetVelocity();

    // do we have enough time to react to it?
    if (distToProj > projDir.Magnitude()*INCOMING_PROJECTILE_DODGE_REACT_TIME)
    {
      continue;
    }

    if ((myDir^toProj) < INCOMING_PROJECTILE_FOV_DOT)
    {
      continue;
    }

    projDir.Y = 0.0f;
    projDir.Normalize();
    dotProjDir = toProj^projDir;
    if (dotProjDir > -INCOMING_PROJECTILE_DODGE_DOT)
    {
      if (distToProj <= INCOMING_PROJECTILE_FROMBEHIND_NEAR)
      {
        bProjFromBehind = true;
        fromBehindDir = projDir;
      }

      continue;
    }

    // found incoming
    dodge.Cross(myDir, SyVect3(0.0f, 1.0f, 0.0f));

    if ((dodge ^ toProj) >= 0.0f)
    {
      dodge = -dodge;
    }

    static_cast<cIntelNPC*>(mpOwner->GetIntel())->Dodge(dodge);    
    mLastDodgedProjectileID = pProjectile->GetID();

    if (GetAttackTarget()==NULL && mGoalQueue.GetHighestPriority() <= 1)
    {
      // if we're not doing anything hunt them down, but only if it's not a trap that shot it
      projDir *= 10.0f;
      tGameObjectID sourceID = static_cast<cPhysicsProjectile*>(pProjectile->GetPhysics())->GetSourceID();
      cGameObject* pSource = pRegistry->Fetch(sourceID);
      if (pSource && (pSource->GetType() == cGameObject::OBJ_NPC || pSource->GetType() == cGameObject::OBJ_PLAYER))
      {
        SetAttackTarget(sourceID);
        AttackTarget(9);
        Hunt(10, sourceID, myLoc-projDir);
      }
    }

    return;
  }

  if (bProjFromBehind)
  {
    if (GetAttackTarget()==NULL && mGoalQueue.GetHighestPriority() <= 1)
    {
      static_cast<cIntelNPC*>(mpOwner->GetIntel())->TurnTo(myLoc-fromBehindDir);
    }
  }
}

void cAi::CheckForNegativeAreaEffects()
{
  cAIBlackboard::Record rec;
  SyVect3 myLoc = mpOwner->GetLocation();
  tGameObjectID myID = mpOwner->GetID();
  cGameObjectRegistry* pRegistry = mpOwner->GetRegistry();
  cGameObject* pSourceObj = NULL;
  
  bool bFoundNegativeAreaEffect = false;
  SyVect3 effectLocation(0.0f, 0.0f, 0.0f);
  float effectRadius = 0.0f;

  if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_NEGATIVE_AREAEFFECT, ID_NONE, ID_NONE, rec))
  {
    do
    {
      if (rec.mSourceID != myID)
      {
        pSourceObj = NULL;
        if (rec.mSourceID != ID_NONE)
        {
          pSourceObj = pRegistry->Fetch(rec.mSourceID);
        }

        effectRadius = (float)rec.mData;
        if (rec.mLocation.DistanceSquared(myLoc) < effectRadius*effectRadius)
        {
          bFoundNegativeAreaEffect = true;
          effectLocation = rec.mLocation;
        }
      }
    }
    while (!bFoundNegativeAreaEffect && cAIBlackboard::Get()->GetNextRecord(rec));
  }

  if (bFoundNegativeAreaEffect)
  {
    if (pSourceObj != NULL )
    {
      Flee(mGoalQueue.GetHighestPriority()+1, pSourceObj->GetID(), 0.0f, effectRadius+2.0f);
    }
    else
    {
      Flee(mGoalQueue.GetHighestPriority()+1, effectLocation, 0.0f, effectRadius+2.0f);
    }
  }
}

void cAi::OnAttacked(tGameObjectID attacker, int damage)
{
  cGameObject* pAttackerObj = mpOwner->GetRegistry()->Fetch(attacker);

  if (!pAttackerObj || mpOwner->GetIntel()->IsFriendly(pAttackerObj))
  {
    return;
  }

  if (GetAttackTarget()==NULL && mGoalQueue.GetHighestPriority() <= 1)
  {
    // if we're not doing anything hunt them down, but only if it's not a trap that shot it
    if (pAttackerObj && (pAttackerObj->GetType() == cGameObject::OBJ_NPC || pAttackerObj->GetType() == cGameObject::OBJ_PLAYER))
    {
      SetAttackTarget(attacker);
      AttackTarget(9);
    }
  }
}

void cAi::AttackTarget(int priority)
{
  SyAssertf(ID_NONE != mAttackTarget.mTargetID, "cAi::AttackTarget - must call SetTarget before AttackTarget");

  if (ID_NONE == mAttackTarget.mTargetID)
  {
    return;
  }

  cGameObject *attacker_obj = mAttackTarget.GetTarget(mpOwner); 
  SyAssertf(attacker_obj != NULL,"Bad attacker ID");

  if (attacker_obj == NULL)
  {
    return;
  }

  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_ATTACK);
  if (NULL != pGoal)
  {
    if (static_cast<cGoal_Attack*>(pGoal)->GetTarget() == attacker_obj)
    {
      if (priority > pGoal->GetPriority())
      {
        mGoalQueue.ChangePriority(pGoal, priority);
      }

      return;
    }
    else
    {
      Pop(pGoal);
    }
  }
  else
  {
    cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_CAST);
    if (NULL != pGoal)
    {
      if (priority > pGoal->GetPriority())
      {
        mGoalQueue.ChangePriority(pGoal, priority);
      }

      return;
    }
  }

  char behType = static_cast<cStatsCharacter*>(mpOwner->GetStats())->GetBehaviorType();

  switch (behType)
  {
    case NPCBEH_MAXTYPES:
      SyAssertf(false, "Bad Behavior Type in cAI::AttackTarget");
      break;

    case NPCBEH_NONE:
    case NPCBEH_RUNNER:
    case NPCBEH_CAST_SUMMON:
    case NPCBEH_CAST_EXPLOSION:
    case NPCBEH_CAST_HEAL:
      break;

    case NPCBEH_RANGEDONLY:
    case NPCBEH_MELEERANGED: 
      pGoal = new cGoal_Ranged(attacker_obj->GetID());
      break;

    case NPCBEH_RANGEDLINE:
      pGoal = new cGoal_RangedLine(attacker_obj->GetID());
      break;

    case NPCBEH_RANGEDSTUN:
      pGoal = new cGoal_RangedStun(attacker_obj->GetID());
      break;

    case NPCBEH_RANGEDCIRCLE:
      pGoal = new cGoal_RangedCircle(attacker_obj->GetID());
      break;

    case NPCBEH_RANGEDTELEPORT:
      pGoal = new cGoal_RangedTeleport(attacker_obj->GetID());
      break;

    case NPCBEH_MELEESWARM:
      pGoal = new cGoal_MeleeSwarm(attacker_obj->GetID());
      break;

    case NPCBEH_MELEEFLANK:
      pGoal = new cGoal_MeleeFlank(attacker_obj->GetID());
      break;

    case NPCBEH_MELEECHARGERETREAT:
      pGoal = new cGoal_MeleeChargeRetreat(attacker_obj->GetID());
      break;

    case NPCBEH_MELEEENRAGE:
      pGoal = new cGoal_MeleeEnrage(attacker_obj->GetID());
      break;

    case NPCBEH_MELEESTEALTH:
      pGoal = new cGoal_MeleeStealth(attacker_obj->GetID());
      break;

    case NPCBEH_MELEESTUN:
      pGoal = new cGoal_MeleeStun(attacker_obj->GetID());
      break;

    case NPCBEH_MELEEWANDER:
      pGoal = new cGoal_MeleeWander(attacker_obj->GetID());
      break;

    case NPCBEH_KAMIKAZE:
      pGoal = new cGoal_Kamikaze(attacker_obj->GetID());
      break;

    case NPCBEH_MELEEONLY:
    default:
      pGoal = new cGoal_Melee(attacker_obj->GetID());
      break;
  }

  if (pGoal)
  {
    mGoalQueue.Push(priority, pGoal);
  }
}


void cAi::GoTo(int priority, const SyVect3 &destination)
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_GOTO);

  if (NULL == pGoal)
  {
    cGoal_Goto *newgoal = new cGoal_Goto(destination);
    mGoalQueue.Push(priority,newgoal);
  }
}

void cAi::Wander(int priority, float time)
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_WANDER);

  if (NULL == pGoal)
  {
    cGoal_Wander *newgoal = new cGoal_Wander(time);
    mGoalQueue.Push(priority,newgoal);
  }
}

void cAi::Follow(int priority, tGameObjectID targetID)
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_FOLLOW);

  if (NULL == pGoal)
  {
    cGoal_Follow *newgoal = new cGoal_Follow(targetID);
    mGoalQueue.Push(priority, newgoal);
  }
}

void cAi::Defend(int priority, tGameObjectID targetID)
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_FOLLOW);

  if (NULL == pGoal)
  {
    cGoal_Defend *newgoal = new cGoal_Defend(targetID);
    mGoalQueue.Push(priority, newgoal);
  }
}

void cAi::Hunt(int priority, tGameObjectID targetID, const SyVect3& startLoc)
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_HUNT);

  if (NULL == pGoal)
  {
    cGoal_Hunt *newgoal = new cGoal_Hunt(targetID, startLoc);
    mGoalQueue.Push(priority, newgoal);
  }
}

void cAi::PatrolAddWaypoint(const SyVect3& markerLoc, float speed, float pauseTime)
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_PATROL);

  int priority = 1;  
  if (static_cast<cStatsCharacter*>(mpOwner->GetStats())->GetBehaviorType() == NPCBEH_RUNNER)
  {
    priority = 100;
  }

  if (!pGoal)
  {
    pGoal = new cGoal_Patrol();
    mGoalQueue.Push(priority, pGoal);
  }
  else
  {
    ChangePriority(pGoal, priority);
  }

  static_cast<cGoal_Patrol*>(pGoal)->AddWaypoint(markerLoc, speed, pauseTime);
}

void cAi::PatrolClearWaypoints()
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_PATROL);

  if (NULL != pGoal)
  {
    static_cast<cGoal_Patrol*>(pGoal)->ClearWaypoints();
  }
}

void cAi::PatrolSetLooping(bool bLoop)
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_PATROL);

  int priority = 1;  
  if (static_cast<cStatsCharacter*>(mpOwner->GetStats())->GetBehaviorType() == NPCBEH_RUNNER)
  {
    priority = 100;
  }

  if (!pGoal)
  {
    pGoal = new cGoal_Patrol();
    mGoalQueue.Push(priority, pGoal);
  }
  else
  {
    ChangePriority(pGoal, priority);
  }

  static_cast<cGoal_Patrol*>(pGoal)->SetLooping(bLoop);
}

void cAi::FadeAway(int priority)
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_FADEAWAY);

  if (NULL == pGoal)
  {
    cGoal_Fade *newgoal = new cGoal_Fade();
    mGoalQueue.Push(priority, newgoal);
  }
}

void            
cAi::Stand(int priority)  // maybe BurnOut would be better?
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_STAND);

  if (pGoal != NULL)
  {
    mGoalQueue.ChangePriority(pGoal, priority);
  }
  else
  {
    cGoal_Stand *newgoal = new cGoal_Stand();
    mGoalQueue.Push(priority, newgoal);
  }
}
  
void cAi::Flee(int priority, tGameObjectID targetID, float time, float goalDistance) 
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_FLEE);

  if (NULL == pGoal)
  {
    cGoal_Flee *newgoal = new cGoal_Flee(targetID, time, goalDistance);
    mGoalQueue.Push(priority, newgoal);
  }
}

void cAi::Flee(int priority, const SyVect3& pos, float time, float goalDistance) 
{
  cAiGoal* pGoal = mGoalQueue.FindGoalType(GOAL_FLEE);

  if (NULL == pGoal)
  {
    cGoal_Flee *newgoal = new cGoal_Flee(pos, time, goalDistance);
    mGoalQueue.Push(priority, newgoal);
  }
}

bool cAi::TestMaxClosestAttackers(cAIBlackboard::RecordType type,
                             int maxAttackers,
                             tGameObjectID targetID,
                             float maxDist)
{
  cAIBlackboard::Record rec;
  int numCloserAttackers = 0;
  float distSqr = maxDist*maxDist;
  SyVect3 targetLoc(0.0f, 0.0f, 0.0f);

  cGameObject* pAttacker;
  cGameObject* pTarget = mpOwner->GetRegistry()->Fetch(targetID);

  SyAssertf(pTarget != NULL, "cAi::TestMaxClosestAttackers has invalid target %d", targetID);
  if (pTarget)
  {
    targetLoc = pTarget->GetLocation();
  }

  if (cAIBlackboard::Get()->GetFirstRecord(type, ID_NONE, targetID, rec))
  { 
    do
    {
      pAttacker = mpOwner->GetRegistry()->Fetch(rec.mSourceID);        
      if (pAttacker != mpOwner &&
          pAttacker->GetLocation().DistanceSquared(targetLoc) < distSqr)
      {
        ++numCloserAttackers;
      }
    }
    while (cAIBlackboard::Get()->GetNextRecord(rec));
  }

  return numCloserAttackers < maxAttackers;
}

bool
cAi::IsOnCamera()
{
  SyCamera* pCam = mpOwner->GetTitan()->GetCameraController()->GetCamera();

  float maxCameraDistSqr = 50.0f*50.0f;

  if (pCam &&
      pCam->GetLocation().DistanceSquared(mpOwner->GetLocation()) < maxCameraDistSqr &&
      pCam->GetFrustum().Cull(mpOwner->GetLocation(), 1.0f) == 0)
  {
    return true;
  }

  return false;
}

bool cAi::IsPointInFOV(const SyVect3& point)
{
  static const float FOV_COS = 0.5f;

  SyVect3 toPoint(point - mpOwner->GetLocation());
  toPoint.Normalize();

  SyVect3 myDir;
  myDir.HPR(mpOwner->GetHeading(), 0.0f, 0.0f);

  return ((toPoint^myDir) >= FOV_COS);
}

bool cAi::IsRangedAttacker()
{
  switch (static_cast<cStatsCharacter*>(mpOwner->GetStats())->GetBehaviorType())
  {
    case NPCBEH_RANGEDONLY:
    case NPCBEH_MELEERANGED:
    case NPCBEH_RANGEDCIRCLE:
    case NPCBEH_RANGEDLINE:
    case NPCBEH_RANGEDSTUN:
    case NPCBEH_RANGEDTELEPORT:
    case NPCBEH_CAST_SUMMON:
    case NPCBEH_CAST_EXPLOSION:
      return true;

    default: 
      return false;
  }
}

bool cAi::IsMeleeAttacker()
{
  switch (static_cast<cStatsCharacter*>(mpOwner->GetStats())->GetBehaviorType())
  {
    case NPCBEH_MELEESWARM:
    case NPCBEH_MELEEFLANK:
    case NPCBEH_MELEECHARGERETREAT:
    case NPCBEH_KAMIKAZE:
    case NPCBEH_MELEESTEALTH:
    case NPCBEH_RUNNER:
    case NPCBEH_MELEEONLY:
    case NPCBEH_MELEEENRAGE:
    case NPCBEH_MELEESTUN:
    case NPCBEH_MELEEWANDER:
      return true;

    default:
      return false;
  }
}

void cAi::CalculateFleeDirection(cGameObject* pTarget, SyVect3& fleeDir)
{
  static const float NPC_RADIUS_SQR = 20.0f*20.0f;

  cGameObjectRegistry* pRegistry = mpOwner->GetRegistry();

  SyAssert(pRegistry != NULL);

  if (!pRegistry)
  {
    return;
  }

  int endReg = pRegistry->End();
  cGameObject* pObj;
  cIntelNPC* pMyIntel = static_cast<cIntelNPC*>(mpOwner->GetIntel());
  cAi* pObjAi;
  int numMelee = 0;
  int numRanged = 0;
  SyVect3 meleeDir(0.0f, 0.0f, 0.0f), rangedDir(0.0f, 0.0f, 0.0f);
  SyVect3 myLoc(mpOwner->GetLocation());

  for (int i=pRegistry->Begin(); i!=endReg; i=pRegistry->Next(i))
  {
    pObj = (*pRegistry)(i);

    if (!pObj ||
        pObj == mpOwner ||
        pObj->GetType() != cGameObject::OBJ_NPC ||
        pObj->GetStats()->IsDead() ||
        !pMyIntel->IsFriendly(pObj) ||
        myLoc.DistanceSquared(pObj->GetLocation()) > NPC_RADIUS_SQR)
    {
      continue;
    }

    pObjAi = static_cast<cAi*>(prop_cast<cIntelNPC*>(pObj->GetIntel())->GetAi());
      
    if (pObjAi->IsRangedAttacker())
    {
      rangedDir += pObj->GetLocation();
      ++numRanged;
    }
    else if (pObjAi->IsMeleeAttacker())
    {
      meleeDir += pObj->GetLocation();
      ++numMelee;
    }
  }

  // check for running into target, too
  static const float STUPID_FLEE_DOT_TOLERANCE = 0.7f;
  SyVect3 targetDir(0.0f, 0.0f, 0.0f), myDir;
  
  if (pTarget)
  {
    targetDir = pTarget->GetLocation()-myLoc;
    targetDir.Normalize();
  }

  float closestToCurrentDirection, dot;

  myDir.HPR(mpOwner->GetHeading(), mpOwner->GetPitch(), mpOwner->GetRoll());
  fleeDir = -targetDir;
  closestToCurrentDirection = fleeDir ^ myDir;

  if (numMelee > 0)
  {
    meleeDir /= (float)numMelee;
    meleeDir = myLoc - meleeDir;
    meleeDir.Normalize();
    if (((meleeDir^targetDir) < STUPID_FLEE_DOT_TOLERANCE))
    {
      dot = meleeDir ^ myDir;
      if (dot >= closestToCurrentDirection)
      {
        closestToCurrentDirection = dot;
        fleeDir = meleeDir;
      }
    }
  }

  if (numRanged > 0)
  {
    rangedDir /= (float)numRanged;
    rangedDir = myLoc - rangedDir;
    rangedDir.Normalize();
    if (((rangedDir^targetDir) < STUPID_FLEE_DOT_TOLERANCE))
    {
      dot = rangedDir ^ myDir;
      if (dot >= closestToCurrentDirection)
      {
        closestToCurrentDirection = dot;
        fleeDir = rangedDir;
      }
    }
  }
}

float cAi::GetActivationRadius()
{
  float override = prop_cast<cStatsCharacter*>(mpOwner->GetStats())->GetMaster()->mActivationRadius;

  if (override >= 0.1f)
  {
    return override;
  }
  
  return smActivationRadius;
}

float cAi::GetDefendRadius()
{
  float override = prop_cast<cStatsCharacter*>(mpOwner->GetStats())->GetMaster()->mDefendRadius;

  if (override >= 0.1f)
  {
    return override;
  }

  return smDefendRadius;
}
//------------------------------------------- cAiGoal

cAiGoal::cAiGoal()
: mpOwner(NULL),
  mPriority(0)
{
}

cAiGoal::~cAiGoal()
{
  mpOwner = NULL;
}

void cAiGoal::Init(cAi *pOwner,int priority)
{
  mpOwner = pOwner;
  mPriority = priority;
}

void cAiGoal::SetPriority(int priority)
{
  mPriority=priority;
}

cIntelNPC* cAiGoal::GetIntel()
{
  SyAssert(prop_cast<cIntelNPC*>(GetGameObject()->GetIntel())!=NULL);
  return static_cast<cIntelNPC*>(GetGameObject()->GetIntel());
}

cAnimCharControllerInterface* cAiGoal::GetAnimInterface()
{
  cGraphicCharacter* pCharGraphic = prop_cast<cGraphicCharacter *>(GetGameObject()->GetGraphic());
  SyAssert(pCharGraphic!=NULL);

  if (pCharGraphic != NULL)
  {
    return pCharGraphic->GetAnimController();
  }

  return NULL;
}


//--------------------------------------------------------cAiLOSMemory
cAiLOSMemory::cAiLOSMemory()
: mTargetID(ID_NONE),
  mLastSeenTargetTime(0.0f),
  mLastSeenTargetPos(0.0f, 0.0f, 0.0f),
  mTargetLOSTicket(-1),
  mbHasLOS(false),
  mbHasSeenTarget(false),
  mbIsTargetInvisible(false),
  mbIsTouchingInvisibleTarget(false),
  mbUseTargetPos(false),
  mTargetPos(0.0f, 0.0f, 0.0f)
{
}

cAiLOSMemory::~cAiLOSMemory()
{
  Clear();
}

void cAiLOSMemory::SetTarget(tGameObjectID targetID)
{
  if (mTargetID != targetID)
  {
    mTargetID = targetID;
    mbHasLOS = false;
    mbHasSeenTarget = false;
    mbUseTargetPos = false;
    mbIsTargetInvisible = false;
    mbIsTouchingInvisibleTarget = false;
  }
}

void cAiLOSMemory::Clear()
{
  mTargetID = ID_NONE;
  mLastSeenTargetTime = 0.0f;
  mLastSeenTargetPos(0.0f, 0.0f, 0.0f);
  mbHasLOS = false;
  mbHasSeenTarget = false;
  mbIsTargetInvisible = false;
  mbIsTouchingInvisibleTarget = false;
  mbUseTargetPos = false;
  mTargetPos(0.0f, 0.0f, 0.0f);

  if (mTargetLOSTicket >= 0)
  {
    cLOSTable::CancelLOS(mTargetLOSTicket);
    mTargetLOSTicket = -1;
  }
}

void cAiLOSMemory::CancelLOSRequest()
{
  if (mTargetLOSTicket >= 0)
  {
    cLOSTable::CancelLOS(mTargetLOSTicket);
    mTargetLOSTicket = -1;
  }
}

void cAiLOSMemory::SetTargetPosition(const SyVect3& targetPos)
{
  static const float LOS_TARGET_POSITION_TOLERANCE = 0.1f*0.1f;

  mbUseTargetPos = true;
  mTargetID = ID_NONE;
  mbIsTargetInvisible = false;
  mbIsTouchingInvisibleTarget = false;

  if (mTargetPos.DistanceSquared(targetPos) > LOS_TARGET_POSITION_TOLERANCE)
  {
    mTargetPos = targetPos;
    mbHasLOS = false;
    mbHasSeenTarget = false;
  }
}

cGameObject* cAiLOSMemory::GetTarget(cGameObject* pOwner)
{
  if (ID_NONE != mTargetID)
  {
    cGameObject* pTarget = pOwner->GetRegistry()->Fetch(mTargetID); 
    if (pTarget)
    {
      return pTarget;
    }
    else
    {
      mTargetID = ID_NONE;  // got deleted out from under us
    }
  }

  return NULL;
}


bool cAiLOSMemory::CanSeeTarget()
{
  if (mbUseTargetPos)
  {
    return mbHasLOS;
  }
  else if (mTargetID != ID_NONE)
  {
    return mbHasLOS && (!mbIsTargetInvisible || mbIsTouchingInvisibleTarget);
  }
  else
  {
    return false;
  }
}

bool cAiLOSMemory::HasLOS()
{
  if (mTargetID != ID_NONE || mbUseTargetPos)
  {
    return mbHasLOS;
  }

  return false;
}


bool cAiLOSMemory::IsLOSPending()
{
  if ((mTargetID != ID_NONE || mbUseTargetPos) &&
      mTargetLOSTicket >= 0)
  {
    return cLOSTable::HasLOS(mTargetLOSTicket) == cLOSTable::LOS_PENDING;
  }

  return cLOSTable::LOS_INVALID;
}

bool cAiLOSMemory::GetLastSeenTargetPos(SyVect3& pos)
{
  if (mbHasSeenTarget)
  {
    pos = mLastSeenTargetPos;
  }

  return mbHasSeenTarget;
}

void cAiLOSMemory::Update(cGameObject* pOwner, float time)
{
  static const float INVISIBLE_TOUCHING_DISTANCE = 0.25f;

  bool bIsDead = pOwner->GetStats()->IsDead();
  cGameObject* pTarget = GetTarget(pOwner); 

  SyVect3 targetPos;
  SyVect3 height(0.0f, 1.0f, 0.0f);
  bool bHasTarget = false;

  mbIsTargetInvisible = false;
  mbIsTouchingInvisibleTarget = false;

  if (NULL != pTarget)
  {
    if (!pTarget->GetStats()->IsDead())
    {
      bHasTarget = true;
      targetPos = pTarget->GetLocation();

      // test for an invisible target, and if we're touching him
      if (pTarget->GetType() == cGameObject::OBJ_NPC ||
          pTarget->GetType() == cGameObject::OBJ_PLAYER)
      {
        mbIsTargetInvisible = prop_cast<cStatsCharacter*>(pTarget->GetStats())->HasCondition("Invisible");
        if (mbIsTargetInvisible && pOwner->GetDistance(pTarget) < INVISIBLE_TOUCHING_DISTANCE)
        {
          mbIsTouchingInvisibleTarget = true;
          mbHasSeenTarget = true;
          mLastSeenTargetPos = targetPos;
        }
      }
    }
  }
  else if (mbUseTargetPos)
  {
    bHasTarget = true;
    targetPos = mTargetPos;
  }

  if (bHasTarget && !bIsDead)
  {
    if (mTargetLOSTicket >= 0)
    {   
      cLOSTable::LOSStatus losStatus = cLOSTable::HasLOS(mTargetLOSTicket);

      if (cLOSTable::LOS_CLEAR == losStatus)
      {
        mbHasLOS = true;
        mbHasSeenTarget = true;
        mLastSeenTargetPos = targetPos;
        cLOSTable::CancelLOS(mTargetLOSTicket);
        mTargetLOSTicket = -1;
//          mLastSeenTargetTime =??;

//          AIDEBUGDRAW_LINE(pOwner->GetID(), pOwner->GetLocation()+height, targetPos+height, cAIDebugDraw::GREEN);
      }
      else if (cLOSTable::LOS_BLOCKED == losStatus)
      {
        mbHasLOS = false;
        cLOSTable::CancelLOS(mTargetLOSTicket);
        mTargetLOSTicket = -1;

//          AIDEBUGDRAW_LINE(pOwner->GetID(), pOwner->GetLocation()+height, targetPos+height, cAIDebugDraw::RED);
      }
      else
      {
//          AIDEBUGDRAW_LINE(pOwner->GetID(), pOwner->GetLocation()+height, targetPos+height, cAIDebugDraw::YELLOW);
      }
    }

    if (mTargetLOSTicket < 0)
    {
      cLOSTable::RequestLOS(pOwner->GetLocation()+height, targetPos+height, mTargetLOSTicket);      
    }
  }
  else      // target or we died
  {
    mbHasLOS = false;
    mbHasSeenTarget = false;
    mTargetID = ID_NONE;

    if (mTargetLOSTicket >= 0)
    {
      cLOSTable::CancelLOS(mTargetLOSTicket);
      mTargetLOSTicket = -1;
    }
  }
}

cAIWanderSteering::cAIWanderSteering()
: mWanderHPR(0.0f, 0.0f, 0.0f),
  mWanderRate(0.8f),
  mLastWanderAdjust(0.0f)
{
}

void cAIWanderSteering::Init(cGameObject* pNPC, float baseRate, float randAdjust)
{
  mLastWanderAdjust = 0.0f;
  mWanderHPR.HPR(pNPC->GetHeading(), 0.0f, 0.0f);

  // randomize wander rate (variance in direction each frame)
  // to make each npc look a little different
  mWanderRate = (((float)(pNPC->GetTitan()->Random(0, (int)(randAdjust*100.0f))))/100.0f) - (randAdjust*0.5f) + baseRate;
}

void cAIWanderSteering::SetHPR(const SyVect3& hpr)
{
  mWanderHPR = hpr;
}

void cAIWanderSteering::MoveNPC(cGameObject* pNPC, float speed, float radius)
{
  SyAssert(pNPC != NULL);
  
  SyVect3 myDir, wanderOffsetLoc, wanderDir;
  myDir.HPR(pNPC->GetHeading(), 0.0f, 0.0f);

  // the wander angle is just the distance of the arc length we want (the wander rate)
  float randAngle = ((((float)(pNPC->GetTitan()->Random(0, (int)(mWanderRate*2.0f*100.0f))))/100.0f) - mWanderRate);            
  cIntelNPC* pIntel = prop_cast<cIntelNPC*>(pNPC->GetIntel());

  if (pIntel->IsAvoiding())
  {
    if ((randAngle < 0.0f && mLastWanderAdjust > 0.0f) ||
        (randAngle < 0.0f && mLastWanderAdjust > 0.0f))
    {
      randAngle = -randAngle;
    }
  }

  mLastWanderAdjust = randAngle;
  mWanderHPR.X += randAngle;
  wanderDir.HPR(mWanderHPR.X, mWanderHPR.Y, mWanderHPR.Z);
  wanderDir *= radius;

//  AIDEBUGDRAW_LINE(GetGameObject()->GetID(), myLoc+myDir+SyVect3(0.0f, 3.0f, 0.0f), myLoc+myDir+SyVect3(0.0f, 3.0f, 0.0f)+wanderDir, cAIDebugDraw::WHITE);
//  AIDEBUGDRAW_LINE(GetGameObject()->GetID(), myLoc+SyVect3(0.0f, 3.0f, 0.0f), myLoc+myDir+SyVect3(0.0f, 3.0f, 0.0f), cAIDebugDraw::MAGENTA);
//  AIDEBUGDRAW_LINE(GetGameObject()->GetID(), myLoc+SyVect3(0.0f, 3.0f, 0.0f), myLoc+myDir+wanderDir+SyVect3(0.0f, 3.0f, 0.0f), cAIDebugDraw::CYAN);

  myDir *= radius + 1.0f;
  wanderDir += myDir;
  wanderDir *= 3.0f;

  if (speed <= 0.0f)
  {
    pIntel->Stop();
    pIntel->TurnTo(pNPC->GetLocation()+wanderDir);
  }
  else
  {
    pIntel->GoTo(pNPC->GetLocation()+wanderDir, speed);
  }
}

void cAi::RegisterTuningVariables()
{
  gTuningSys.AddFloat(&smDefendRadius, "AI_DefendRadius");
  gTuningSys.AddFloat(&smActivationRadius, "AI_ActivationRadius");
}

void cAiInterface::RegisterTuningVariables()
{
  cAi::RegisterTuningVariables();
  cGoal_Ranged::RegisterTuningVariables();
}


// EOF
