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

//-------------------------------------------------------- Includes
#include "aigoal_attack.h"

#include "SyBBox.h"
#include "SyCollRay.h"
#include "SyScene.h"

#include "../intel.h"
#include "../stats.h"
#include "../graphic.h"
#include "../physics.h"
#include "../inventory.h"
#include "../registry.h"
#include "../database.h"
#include "../titan.h"
#include "../areaeffect.h"
#include "../tuning.h"
#include "../rule_condition.h"

#include "behaviortypes.h"
#include "aidebugdraw.h"
#include "aiblackboard.h"
//---------------------------------------------- Class Declarations

//------------------------------------------- cGoal_Attack
cGoal_Attack::cGoal_Attack(tGameObjectID target)
: cAiGoal(),
  mTargetID(target),
  mAttackTime(0.0f),
  mBlockTime(0.0f),
  mbTargetWasAttacking(false)
{
}

void cGoal_Attack::Enter()
{
  mAttackTime = 0.0f;
  mBlockTime = 0.0f;
  mbTargetWasAttacking = false;

  // do subclass tasks now
  AttackEnter();
}

void cGoal_Attack::Exit()
{
  // do subclass tasks now
  AttackExit();
}

cGameObject* cGoal_Attack::GetTarget()
{
  return GetRegistry()->Fetch(mTargetID);
}

bool cGoal_Attack::CheckAttackDelay(float time)
{
  bool bCanAttack = true;

  cAnimCharControllerInterface *iface = GetAnimInterface();

  if (iface->IsAttacking())
  {
    mAttackTime = ((cStatsCharacter*)GetGameObject()->GetStats())->GetAttackDelay();
    GetIntel()->Stop();
    bCanAttack = false;
  }
  else if (mAttackTime > time)
  {
    mAttackTime -= time;
    bCanAttack = false;
  }
  else
  {
    mAttackTime = 0.0f;
  }

  return bCanAttack;
}

bool cGoal_Attack::CheckBlockOpportunity(float time)
{
  static const float BLOCK_FOV_COS = SY_COS(SY_DEG_TO_RAD((180.0f*0.5f)));
  static const float ATTACK_FOV_COS = SY_COS(SY_DEG_TO_RAD((120.0f*0.5f)));
  static const float MAX_BLOCK_RANGE = 4.0f;

  cAnimCharControllerInterface* pAnimCtrlr = GetAnimInterface();
  bool bIsBlocking = pAnimCtrlr->IsBlocking() || pAnimCtrlr->GetAnimState() == AS_DODGE;

//  AIDEBUGDRAW_LINE(GetGameObject()->GetID(), GetGameObject()->GetLocation(), GetGameObject()->GetLocation()+SyVect3(0.0f, 3.0f, 0.0f), pAnimCtrlr->IsBlocking()?cAIDebugDraw::RED:cAIDebugDraw::GREEN);

  if (bIsBlocking)
  {
    mBlockTime = ((cStatsCharacter*)GetGameObject()->GetStats())->GetBlockDelay();
  }
  else if (mBlockTime > time)
  {
    mBlockTime -= time;
  }
  else
  {
    mBlockTime = 0.0f;
  }

  cGameObject* pTarget = GetTarget();
  SyAssert(prop_cast<cStatsCharacter*>(GetGameObject()->GetStats())!=NULL);

  cAnimCharControllerInterface *pTargetAnimCtrlr = prop_cast<cGraphicCharacter*>(pTarget->GetGraphic())->GetAnimController();

  cStatsCharacter *pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  float blockPercentRemain = ((float)pStats->GetBlock())/((float)pStats->CalcMaxBlock());

  bool bTargetStartedAttack = false;
  bool bTargetStoppedAttack = false;
  bool bTargetIsAttacking = pTargetAnimCtrlr->IsAttacking();

  if (!mbTargetWasAttacking && bTargetIsAttacking)
  {
    bTargetStartedAttack = true;
  }

  if (mbTargetWasAttacking && !bTargetIsAttacking)
  {
    bTargetStoppedAttack = true;
  }

  if (bIsBlocking && bTargetStoppedAttack)
  {
    GetIntel()->StopBlock();
    bIsBlocking = false;
  }
  else if (bIsBlocking && blockPercentRemain < 0.1f)
  {
    GetIntel()->StopBlock();
    bIsBlocking = false;
  }
  else if (!bIsBlocking &&
           mBlockTime <= 0.0f &&
           bTargetStartedAttack)
  {
    SyVect3 toTarget(pTarget->GetLocation()-mpOwner->GetOwner()->GetLocation());

    if (toTarget.Y < 10.0f)
    {
      toTarget.Y = 0.0f;
      float dist = toTarget.NormalizeMagn();

      if (dist < MAX_BLOCK_RANGE)
      {
        SyVect3 myDir, targetFacing;
        bool bDodge = GetRegistry()->GetTitan()->Random(0, 4) == 0;
        myDir.HPR(GetGameObject()->GetHeading(), 0.0f, 0.0f);
        targetFacing.HPR(pTarget->GetHeading(), 0.0f, 0.0f);

        if ((myDir^toTarget)>=BLOCK_FOV_COS &&
            (targetFacing ^(-toTarget)) >= ATTACK_FOV_COS)
        {
          bIsBlocking = true;

          if (blockPercentRemain < 0.1f || bDodge)
          {
            SyVect3 right;
            right.Cross(myDir, SyVect3(0.0f, 1.0f, 0.0f));
            right.Normalize();
            if ((right ^ toTarget) > 0.0f)
            {
              right = -right;
            }

            GetIntel()->Dodge(right);
          }
          else
          {
            GetIntel()->Block(10.0f);
          }
        }
      }
    }
  }

  mbTargetWasAttacking = bTargetIsAttacking;

  return bIsBlocking;
}

bool cGoal_Attack::IsAttackDone()
{
  if (ID_NONE == mTargetID || 
      GetTarget() == NULL || 
      GetTarget()->GetStats()->IsDead() || 
      mpOwner->GetOwner()->GetStats()->IsDead())
  {
    return true;
  }

  return false;
}

bool cGoal_Attack::FindTarget(bool bHuntDown)
{
  cGameObject* pTarget = GetTarget();

  SyAssert(mpOwner->GetAttackTarget() == NULL || mpOwner->GetAttackTarget() == pTarget);

  if (!mpOwner->GetAttackTarget() || !pTarget)
  {
    return false;
  }

  if (pTarget->GetType() == cGameObject::OBJ_PROP)
  {
    return mpOwner->CanSeeAttackTarget();
  }

  SyAssert(pTarget->GetType()==cGameObject::OBJ_NPC || pTarget->GetType() == cGameObject::OBJ_PLAYER);

  if (!mpOwner->CanSeeAttackTarget())
  {
    SyVect3 lastSeenTargetLoc;
    if (mpOwner->GetLastSeenAttackTargetPos(lastSeenTargetLoc) && bHuntDown)
    {
      mpOwner->Hunt(GetPriority()+1, pTarget->GetID(), lastSeenTargetLoc);
    }

    return false;
  }

  return true;
}
//------------------------------------------- cGoal_Melee
cGoal_Melee::cGoal_Melee(tGameObjectID target)
: cGoal_Attack(target)
{
}


void cGoal_Melee::AttackExit()
{
  GetIntel()->StopBlock();
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_ATTACKING_MELEE, mpOwner->GetOwner()->GetID());
}

void 
cGoal_Melee::Update(float time)
{
  if (IsAttackDone() || mpOwner->IsOutsideDefendRadius())
  {
    mpOwner->Pop(this);
    return;
  }

  cIntelNPC *pIntel = GetIntel();
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cGameObject* pTarget = GetTarget();
  cAnimCharControllerInterface *pAnimCtrlr = GetAnimInterface();

  bool bCanAttack = CheckAttackDelay(time);
  bool bIsBlocking = CheckBlockOpportunity(time);

  float range = pAnimCtrlr->GetRange(COMBO_L);
  float dist = pTarget->GetLocation().Distance(mpOwner->GetOwner()->GetLocation());
  float speed = pStats->GetMovementSpeed();
  float closeDist = pStats->GetCloseDistance();

  SyVect3 targetLoc = pTarget->GetLocation();
  SyVect3 myLoc = GetGameObject()->GetLocation();
  SyVect3 lastSeenTargetLoc;

  SyAssertf(prop_cast<cStatsCharacter*>(pTarget->GetStats())!=NULL, "Melee ai needs character for target");

#ifdef _AIDEBUGDRAW
//  SyVect3 close(myLoc-targetLoc), above(0.0f, 4.0f, 0.0f);
//  close.Normalize();
//  close *= closeDist;
//  AIDEBUGDRAW_LINE(mpOwner->GetOwner()->GetID(), targetLoc+close, targetLoc+close+above, dist <= closeDist?cAIDebugDraw::RED:cAIDebugDraw::YELLOW);
#endif

  if (bIsBlocking)
  {
    return;
  }

  // attack logic
  if (dist <= closeDist)
  {
    speed *= 0.1f;
  }

  if (FindTarget(true) && bCanAttack)
  {
    // check to prevent too many attackers at once
    static const int MAX_ATTACKERS = 3;
    if (mpOwner->TestMaxClosestAttackers(cAIBlackboard::BBR_ATTACKING_MELEE, MAX_ATTACKERS, mTargetID, dist))
    {
      pIntel->AttackMelee(pTarget, range);
      MoveToAttackPosition(speed);
      cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_ATTACKING_MELEE,
                                      GetGameObject()->GetID(),
                                      mTargetID, 0, myLoc );
    }
    else
    {
      // try and get closer very slowly so maybe the other attackers will have
      // died by the time we get there, and only if we're on camera
      if (mpOwner->IsOnCamera())
      {
        if (dist > closeDist)
        {
          MoveToAttackPosition(0.01f);
        }
        else
        {
          pIntel->TurnTo(targetLoc);
        }
      }
    }
  }
}

bool cGoal_Melee::MoveToAttackPosition(float speed)
{
  SyAssertf(GetTarget() != NULL, "cGoal_Melee::MoveToAttackPosition needs a target");

  GetIntel()->GoTo(GetTarget()->GetLocation(), speed);

  return true;
}

//-------------------------------------------------------- cGoal_MeleeSwarm

bool cGoal_MeleeSwarm::MoveToAttackPosition(float speed)
{
  SyAssertf(GetTarget() != NULL, "cGoal_Melee::MoveToAttackPosition needs a target");

  // pick locations around the player considering attackers positions
  cAIBlackboard::Record rec;
  SyVect3 avoid(0.0f, 0.0f, 0.0f), myLoc(GetGameObject()->GetLocation()), right;
  SyVect3 toTarget(GetTarget()->GetLocation() - myLoc);
  cGameObject* pAttacker;
  int numAttackers = 0;

  toTarget.Normalize();
  right.Cross(toTarget, SyVect3(0.0f, 1.0f, 0.0f));
  right.Normalize();

  if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_ATTACKING_MELEE, ID_NONE, mTargetID, rec))
  { 
    do
    {
      pAttacker = GetRegistry()->Fetch(rec.mSourceID);        
      if (pAttacker != GetGameObject())
      {
        avoid += pAttacker->GetLocation();
        ++numAttackers;
      }
    }
    while (cAIBlackboard::Get()->GetNextRecord(rec));
  }

  if (numAttackers > 0)
  {
    avoid /= (float)(numAttackers);
    avoid = (GetTarget()->GetLocation() - avoid);
    avoid.Normalize();

    if ((avoid ^ right) < 0.0f)
    {
      right = -right;
    }

    avoid = right;
    avoid *= 2.5f;

    GetIntel()->GoTo(GetTarget()->GetLocation()+avoid, speed);
  }
  else
  {
    GetIntel()->GoTo(GetTarget()->GetLocation(), speed);
  }

  return true;
}

//-------------------------------------------------------- cGoal_MeleeFlank
cGoal_MeleeFlank::cGoal_MeleeFlank(tGameObjectID target)
: cGoal_Melee(target),
  mFlankPartnerID(ID_NONE),
  mbRequestFlanking(false)
{
}

void cGoal_MeleeFlank::AttackExit()
{
  cGoal_Melee::AttackExit();

  tGameObjectID myID = GetGameObject()->GetID();
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_FLANK_REQUEST, myID);
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_FLANK_PARTNER, myID);
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_FLANK_PARTNER, ID_NONE, myID);

  mbRequestFlanking = false;
}

bool cGoal_MeleeFlank::MoveToAttackPosition(float speed)
{
  SyAssertf(GetTarget() != NULL, "cGoal_Melee::MoveToAttackPosition needs a target");

  if (ID_NONE != mFlankPartnerID && !mbRequestFlanking)
  {
    // if we already have someone to flank with 
    cGameObject* pFlankPartner = GetRegistry()->Fetch(mFlankPartnerID);

    if (pFlankPartner && !pFlankPartner->GetStats()->IsDead())
    {
      SyVect3 flankAdjust;
      SyVect3 meToTarget = GetTarget()->GetLocation()-GetGameObject()->GetLocation();
      SyVect3 center = pFlankPartner->GetLocation()+GetGameObject()->GetLocation();
      center *= 0.5f;

      SyVect3 centerToTargetDir = GetTarget()->GetLocation() - center;
      centerToTargetDir.Normalize();

      flankAdjust.Cross(centerToTargetDir, SyVect3(0.0f, 1.0f, 0.0f));
      flankAdjust.Normalize();

      if ((meToTarget ^ flankAdjust) >= 0.0f)
      {
        flankAdjust = -flankAdjust;
      }

      float centerSideProj = center.Distance(GetGameObject()->GetLocation());
      float centerForwardProj = meToTarget ^ centerToTargetDir;

      flankAdjust *= centerSideProj - (1.0f-((SY_MIN(centerForwardProj, centerSideProj))/centerSideProj))*centerSideProj;

      //          AIDEBUGDRAW_LINE(mpOwner->GetOwner()->GetID(), GetGameObject()->GetLocation()+SyVect3(0.0f, 3.0f, 0.0f), mpOwner->GetOwner()->GetLocation()+flankAdjust+SyVect3(0.0f, 3.0f, 0.0f), cAIDebugDraw::WHITE);

      GetIntel()->GoTo(GetTarget()->GetLocation()+flankAdjust, speed);          
    }
    else
    {
      mFlankPartnerID = ID_NONE; // he died, so lets look for another partner
    }
  }
  else
  {
    if (ID_NONE != mFlankPartnerID && mbRequestFlanking)
    {
      // if we're waiting for someone to acknowledge
      // our request to partner with them
      cAIBlackboard::Record rec;
      if (!cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_FLANK_PARTNER, GetGameObject()->GetID(), mFlankPartnerID, rec))
      { 
        // record was removing, meaning our request was accepted
        mbRequestFlanking = false;
      }
    }
    else if (ID_NONE == mFlankPartnerID && mbRequestFlanking)
    {
      // we've requested a partner and are just waiting for someone to pick us up

      cAIBlackboard::Record rec;
      if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_FLANK_PARTNER, ID_NONE, GetGameObject()->GetID(), rec))
      { 
        // our request was accepted
        mbRequestFlanking = false;
        mFlankPartnerID = rec.mSourceID;
        cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_FLANK_PARTNER, mFlankPartnerID, GetGameObject()->GetID());

        // other npc should've removed this
        SyAssertf(!cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_FLANK_REQUEST, GetGameObject()->GetID(), mTargetID, rec), "Flanking NPC did not remove request when partnering");
      }
    }
    else
    {
      // first see if anybody else wants to flank vs. this target
      cAIBlackboard::Record rec;
      SyVect3 myLoc(GetGameObject()->GetLocation());
      cGameObject* pFlanker;
      cGameObject* pClosestFlanker = NULL;
      float distSqr, closestDistSqr = 0.0f;

      if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_FLANK_REQUEST, ID_NONE, mTargetID, rec))
      { 
        do
        {
          pFlanker = GetRegistry()->Fetch(rec.mSourceID);        
          if (pFlanker != GetGameObject())
          {
            distSqr = pFlanker->GetLocation().DistanceSquared(myLoc);

            if (!pClosestFlanker || distSqr < closestDistSqr)
            {
              pClosestFlanker = pFlanker;
              closestDistSqr = distSqr;
            }
          }
        }
        while (cAIBlackboard::Get()->GetNextRecord(rec));
      }

      if (pClosestFlanker)
      {
        mFlankPartnerID = pClosestFlanker->GetID();
        mbRequestFlanking = true;
        // remove our partner-to-be's request so no one else snatches it
        cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_FLANK_REQUEST, mFlankPartnerID, mTargetID);
        cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_FLANK_PARTNER, GetGameObject()->GetID(), mFlankPartnerID);
      }
      else
      {
        // no partners, just request one
        mFlankPartnerID = ID_NONE;
        mbRequestFlanking = true;
        cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_FLANK_REQUEST,
                                        GetGameObject()->GetID(),
                                        mTargetID);
      }
    }

    // move towards our target anyway, slowly
    GetIntel()->GoTo(GetTarget()->GetLocation(), 0.01f);
  }

  return true;
}

//-------------------------------------------------------- cGoal_MeleeChargeRetreat
cGoal_MeleeChargeRetreat::cGoal_MeleeChargeRetreat(tGameObjectID target)
: cGoal_Melee(target),
  mbRetreating(false),
  mRetreatTimer(0.0f)
{
}

void cGoal_MeleeChargeRetreat::Update(float time)
{
  if (IsAttackDone() || mpOwner->IsOutsideDefendRadius())
  {
    mpOwner->Pop(this);
    return;
  }

  static const float RETREAT_TIME = 7.0f;

  bool bCanAttack = CheckAttackDelay(time);
  cAnimCharControllerInterface *iface = GetAnimInterface();
  float range = iface->GetRange(COMBO_L);
  float speed = ((cStatsCharacter*)GetGameObject()->GetStats())->GetMovementSpeed();

  if (!mbRetreating && iface->IsAttacking())
  {
    mbRetreating = true;
    mWander.Init(GetGameObject(), 0.2f, 0.01f);

    SyVect3 dir, hpr;
    mpOwner->CalculateFleeDirection(GetTarget(), dir);
    dir.HPR(&hpr.X, &hpr.Y, &hpr.Z);
    mWander.SetHPR(hpr);
  }

  if (mbRetreating)
  {
    mRetreatTimer += time;

    if (mRetreatTimer > RETREAT_TIME)
    {
      mRetreatTimer = 0.0f;
      mbRetreating = false;
    }
    else
    {
      mWander.MoveNPC(GetGameObject(), speed, 2.0f);
    }
  }
  else if (FindTarget(false) && bCanAttack)
  {
    GetIntel()->AttackMelee(GetTarget(), range);
    MoveToAttackPosition(speed);
  }
}

//-------------------------------------------------------- cGoal_MeleeWander
cGoal_MeleeWander::cGoal_MeleeWander(tGameObjectID target)
: cGoal_Melee(target),
  mAttackTimer(0.0f)
{
}

void cGoal_MeleeWander::AttackEnter()
{
  cGoal_Melee::AttackEnter();

  mAttackTimer = 0.0f;
  mWander.Init(GetGameObject(), 0.9f, 0.1f);
}


void cGoal_MeleeWander::Update(float time)
{
  if (IsAttackDone() || mpOwner->IsOutsideDefendRadius())
  {
    mpOwner->Pop(this);
    return;
  }

  cGameObject* pTarget = GetTarget();
  bool bCanAttack = CheckAttackDelay(time);
  cAnimCharControllerInterface *iface = GetAnimInterface();
  float range = iface->GetRange(COMBO_L);
  float speed = ((cStatsCharacter*)GetGameObject()->GetStats())->GetMovementSpeed();
  float distToTarget = GetGameObject()->GetDistance(pTarget);

  static const float MELEEWANDER_MAX_ATTACK_TIME = 3.0f;
  bool bForceWander = false;

  if (mAttackTimer > MELEEWANDER_MAX_ATTACK_TIME)
  {
    bForceWander = true;
  }

  // set ourselves as melee attackers
  cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_ATTACKING_MELEE, GetGameObject()->GetID(), mTargetID, 0, GetGameObject()->GetLocation());

  if (!bForceWander && distToTarget <= range)
  {
    mAttackTimer += time;

    if (FindTarget(false) && bCanAttack)
    {
      // don't test the limit of max attackers - it's your own 
      // dumbass fault for standing next to them!
      GetIntel()->TurnTo(pTarget->GetLocation());
      GetIntel()->AttackMelee(pTarget, range);
    }
  }
  else 
  {
    if (distToTarget > range)
    {
      mAttackTimer = 0.0f;
    }

    static const float MELEE_WANDER_RADIUS = 2.0f;
    mWander.MoveNPC(GetGameObject(), speed, MELEE_WANDER_RADIUS);
  }
}

//-------------------------------------------------------- cGoal_MeleeEnrage
cGoal_MeleeEnrage::cGoal_MeleeEnrage(tGameObjectID target)
: cGoal_Melee(target),
  mbEnraging(false),
  mLastHealthPercent(1.0f)
{
}

void cGoal_MeleeEnrage::AttackEnter()
{
  cGoal_Melee::AttackEnter();

  SyAssert(prop_cast<cStatsCharacter*>(GetGameObject()->GetStats()) != NULL);
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());

  mLastHealthPercent = ((float)pStats->GetHealth())/((float)pStats->CalcMaxHealth());
}

void cGoal_MeleeEnrage::Update(float time)
{
  cGoal_Melee::Update(time);

  SyAssert(prop_cast<cStatsCharacter*>(GetGameObject()->GetStats()) != NULL);
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  float healthPercent = ((float)pStats->GetHealth())/((float)pStats->CalcMaxHealth());

  if (!mbEnraging &&
      healthPercent < 0.5f &&
      healthPercent < mLastHealthPercent)
  {
    mbEnraging = true;
    SyAssert(prop_cast<cGraphicCharacter*>(GetGameObject()->GetGraphic()) != NULL);
  
    cAnimCharControllerInput *pInput = static_cast<cGraphicCharacter*>(GetGameObject()->GetGraphic())->GetAnimController()->GetInput();
    pInput->mEmoteAnger = true;

    cell params[] = {-1000, ID_NONE, 50 };
    GetGameObject()->AddCondition("PartialInvulnerability", 3, params);
  }

  if (mbEnraging)
  {
    mAttackTime = 0.0f;
    mBlockTime = 0.0f;
  }

  mLastHealthPercent = healthPercent;
}

bool cGoal_MeleeEnrage::MoveToAttackPosition(float speed)
{
  return cGoal_Melee::MoveToAttackPosition(mbEnraging ? 1.0f : speed);
}

//-------------------------------------------------------- cGoal_Kamikaze
cGoal_Kamikaze::cGoal_Kamikaze(tGameObjectID target)
: cGoal_Melee(target)
{
}

void cGoal_Kamikaze::AttackEnter()
{
  cGoal_Melee::AttackEnter();
  cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_NEGATIVE_AREAEFFECT, GetGameObject()->GetID(), mTargetID, (unsigned int)(8.0f), GetGameObject()->GetLocation());
}

void cGoal_Kamikaze::AttackExit()
{
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_NEGATIVE_AREAEFFECT, GetGameObject()->GetID());
  cGoal_Melee::AttackExit();
}

void cGoal_Kamikaze::Update(float time)
{
  cGoal_Melee::Update(time);

  // update the position stored on the blackboard
  cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_NEGATIVE_AREAEFFECT, GetGameObject()->GetID(), mTargetID, (unsigned int)(8.0f), GetGameObject()->GetLocation());

  cGameObject* pTarget = GetTarget();

  if (pTarget && pTarget->GetLocation().DistanceSquared(GetGameObject()->GetLocation()) < 4.0f)
  {
    cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_NEGATIVE_AREAEFFECT, GetGameObject()->GetID());
    Explode();
  }
}

void cGoal_Kamikaze::Explode()
{
  Titan* pTitan = GetGameObject()->GetTitan();

  cAreaEffect_Burst *pBurst = new cAreaEffect_Burst(pTitan);
//  pBurst->SetSource(GetGameObject()->GetID());
  pBurst->SetMaxRadius(8.0f);
  pBurst->SetSpeed(20.0f);
  pBurst->SetLocation(GetGameObject()->GetLocation());

  cGameEffect_Damage *pEffect = new cGameEffect_Damage(pTitan);
//  pEffect->SetSource(GetGameObject()->GetID());
  pEffect->SetDamage(10000,NDT_BLUNT, MDT_NONE);
  pBurst->AddGameEffect(pEffect);

  pTitan->GetAreaEffectSys()->Add(pBurst);
}

//-------------------------------------------------------- cGoal_MeleeStealth
cGoal_MeleeStealth::cGoal_MeleeStealth(tGameObjectID target)
: cGoal_Melee(target),
  mbStartedAttack(false),
  mbInvisible(false),
  mbWasAttacking(false),
  mInvisibleTimer(0.0f)
{
};


void cGoal_MeleeStealth::AttackEnter()
{
  cGoal_Melee::AttackEnter();
  mbStartedAttack = false;
  mbWasAttacking = false;
  mInvisibleTimer = 0.0f;
  mTimerInvisible = false;
  SetInvisible(false);
}

void cGoal_MeleeStealth::AttackExit()
{
  SetInvisible(false);
  cGoal_Melee::AttackExit();
}

void cGoal_MeleeStealth::SetInvisible(bool bEnable)
{
  if (mbInvisible != bEnable)
  {
    mbInvisible = bEnable;

    if (mbInvisible)
    {
      GetGameObject()->AddCondition("Invisible", 0, NULL);
    }
    else
    {
      GetGameObject()->RemoveCondition("Invisible");
    }
  }
}

void cGoal_MeleeStealth::Update(float time)
{
  if (!mbStartedAttack)
  {
    if (mpOwner->IsOnCamera() && 0.0f == mInvisibleTimer)
    {
      mInvisibleTimer = 1.25f;
      mTimerInvisible = true;
    }
    
    if (mpOwner->CanSeeAttackTarget())
    {
      GetIntel()->TurnTo(GetTarget()->GetLocation());
    }
  }
  else
  {
    cGoal_Melee::Update(time);
  }

  if (mInvisibleTimer > 0.0f)
  {
    mInvisibleTimer -= time;

    if (mInvisibleTimer <= 0.0f)
    {
      if (!mbStartedAttack)
      {
        mbStartedAttack = true;
      }

      SetInvisible(mTimerInvisible);
      mInvisibleTimer = 0.0f;
    }
  }

  bool bIsAttacking = GetAnimInterface()->IsAttacking();

  if (bIsAttacking && !mbWasAttacking)
  {
    mInvisibleTimer = 0.3f;
    mTimerInvisible = false;
  }
  else if (!bIsAttacking && mbWasAttacking)
  {
    mInvisibleTimer = 0.5f;
    mTimerInvisible = true;
  }

  mbWasAttacking = bIsAttacking;
}

bool cGoal_MeleeStealth::MoveToAttackPosition(float speed)
{
  cGameObject* pTarget = GetTarget();
  SyAssertf(pTarget != NULL, "cGoal_Melee::MoveToAttackPosition needs a target");

  if (!pTarget)
  {
    return true;
  }

  // pick locations around the player considering attackers positions
  SyVect3 targetLoc(GetTarget()->GetLocation());
  SyVect3 targetDir, rightTargetDir, adjust;
  SyVect3 toTarget(targetLoc - GetGameObject()->GetLocation());

  toTarget.Normalize();
  targetDir.HPR(pTarget->GetHeading(), 0.0f, 0.0f);

  adjust = -targetDir;

  if ((toTarget^targetDir) < 0.4f)
  {
    rightTargetDir.Cross(toTarget, SyVect3(0.0f, 1.0f, 0.0f));
    rightTargetDir.Normalize();

    if ((rightTargetDir^targetDir) > 0.0f)
    {
      rightTargetDir = -rightTargetDir;
    }

    rightTargetDir *= 4.0f;
    adjust *= 2.0f;
    adjust += rightTargetDir;
  }


  GetIntel()->GoTo(targetLoc+adjust, speed);

  return true;
}

//-------------------------------------------------------- cGoal_MeleeStun
cGoal_MeleeStun::cGoal_MeleeStun(tGameObjectID target)
: cGoal_Melee(target),
  mbCanIStun(false)
{
}


void cGoal_MeleeStun::AttackEnter()
{ 
  cGoal_Melee::AttackEnter();

  // todo test NPC's weapons
  mbCanIStun = false;
}

void cGoal_MeleeStun::AttackExit()
{
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_STUNNING, GetGameObject()->GetID());
  cGoal_Melee::AttackExit();
}

void cGoal_MeleeStun::Update(float time)
{
  if (IsAttackDone() || mpOwner->IsOutsideDefendRadius())
  {
    mpOwner->Pop(this);
    return;
  }

  cGameObject* pTarget = GetTarget();
  bool bTargetStunned = false;

  if (pTarget)
  {
    SyAssertf(prop_cast<cStatsCharacter*>(pTarget->GetStats())!=NULL, "MeleeStun goal expects a character target");
    cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pTarget->GetStats());
    bTargetStunned = pStats->HasCondition("Stunned");
  }

  if ((bTargetStunned) ||
      (!mbCanIStun && !IsThereAnotherStunner()))
  {
    cGoal_Melee::Update(time);
  }
  else
  {
    if (mbCanIStun)
    {
      //    cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_STUNNING, GetGameObject()->GetID());
    }
    else
    {
      float speed = ((cStatsCharacter*)GetGameObject()->GetStats())->GetMovementSpeed();
      float distToTarget = GetGameObject()->GetDistance(pTarget);

      if (distToTarget < cGoal_Ranged::GetFleeDistance())
      {
        if (!GetIntel()->IsAvoiding())
        {
          SyVect3 fleeDir;
          mpOwner->CalculateFleeDirection(pTarget, fleeDir);
          fleeDir *= 2.0f;
          GetIntel()->GoTo(GetGameObject()->GetLocation()+fleeDir, speed);
        }
      }
      else if (distToTarget > cGoal_Ranged::GetMaxFiringDistance())
      {
        cGoal_Melee::Update(time);
      }
    }
  }
}

bool cGoal_MeleeStun::IsThereAnotherStunner()
{
  cAIBlackboard::Record rec;
  cGameObject* pAttacker;
  bool bFoundAnotherStunner = false;

  // find attackers of same target on either side of me
  if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_STUNNING, ID_NONE, mTargetID, rec))
  { 
    do
    {
      pAttacker = GetRegistry()->Fetch(rec.mSourceID);        
      if (pAttacker != GetGameObject())
      {
        bFoundAnotherStunner = true;
      }
    }
    while (!bFoundAnotherStunner && cAIBlackboard::Get()->GetNextRecord(rec));
  }

  return bFoundAnotherStunner;
}


//-------------------------------------------------------- cGoal_Ranged

float cGoal_Ranged::smFleeDistance = 6.0f;
float cGoal_Ranged::smMaxFiringDistance = 50.0f;

cGoal_Ranged::cGoal_Ranged(tGameObjectID target)
: cGoal_Attack(target),
  mLastFrameTargetLoc(0.0f, 0.0f, 0.0f),
  mTargetAvgVelocity(0.0f, 0.0f, 0.0f),
  mpProjectileMasterStats(NULL)
{
}

void 
cGoal_Ranged::AttackEnter()
{
  cGameObject* pTarget = GetTarget();

  if (pTarget)
  {
    mLastFrameTargetLoc = pTarget->GetLocation();
  }

  mTargetAvgVelocity(0.0f, 0.0f, 0.0f);

  SyAssert(prop_cast<cStatsCharacter*>(GetGameObject()->GetStats()) != NULL);
  tGameID projID = static_cast<cStatsCharacter*>(GetGameObject()->GetStats())->GetProjectileTypeID();
  mpProjectileMasterStats = NULL;

  if (ID_NONE != projID)
  {
    mpProjectileMasterStats = GetGameObject()->GetTitan()->GetDatabaseSys()->GetProjectileMaster(projID);    
  }
  
  SyAssertf(mpProjectileMasterStats != NULL, "Could not find weapon or natural ranged projectile type for NPC %d", GetGameObject()->GetID());
}


void 
cGoal_Ranged::AttackExit()
{
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_ATTACKING_RANGED, GetGameObject()->GetID());
}

void 
cGoal_Ranged::Update(float time)
{
  if (IsAttackDone() || mpOwner->IsOutsideDefendRadius())
  {
    mpOwner->Pop(this);
    return;
  }

  bool bCanAttack = CheckAttackDelay(time);

  SyVect3 targetLoc(GetTarget()->GetLocation());
  float speed = ((cStatsCharacter*)GetGameObject()->GetStats())->GetMovementSpeed();
  float distToTarget = GetGameObject()->GetDistance(GetTarget());
  char behType = ((cStatsCharacter*)GetGameObject()->GetStats())->GetBehaviorType();

  // calculate avg velocity
  static const float AVERAGE_VELOCITY_PERIOD = 1.5f;
  SyVect3 frameVel(targetLoc-mLastFrameTargetLoc);
  SyAssertf(time <= AVERAGE_VELOCITY_PERIOD, "Frame rate assumption incorrect for ranged attack velocity avg.");
  mTargetAvgVelocity *= AVERAGE_VELOCITY_PERIOD-time;
  mTargetAvgVelocity += targetLoc-mLastFrameTargetLoc;
  mTargetAvgVelocity *= (1.0f/AVERAGE_VELOCITY_PERIOD);

//  AIDEBUGDRAW_LINE(GetGameObject()->GetID(), targetLoc+SyVect3(0.0f, 2.5f, 0.0f), targetLoc+mTargetAvgVelocity+SyVect3(0.0f, 2.5f, 0.0f), cAIDebugDraw::CYAN);

  if (!FindTarget(NPCBEH_MELEERANGED == behType))
  {
    // do nothing
  }
  else
  {
    bool bInPositionToAttack = MoveToAttackPosition(speed);
  
    if (NPCBEH_MELEERANGED == behType &&
        distToTarget <= smFleeDistance && bCanAttack && bInPositionToAttack)
    {
      float attackDist = GetAnimInterface()->GetRange(COMBO_L);
      if (distToTarget < attackDist)
      {
        GetIntel()->AttackMelee(GetTarget(), attackDist);
      }
      else
      {      
        GetIntel()->GoTo(targetLoc, speed);
      }
    }
    else if (bCanAttack)
    {
      cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_ATTACKING_RANGED,
                                      GetGameObject()->GetID(),
                                      mTargetID, 0, GetGameObject()->GetLocation());

      if (bInPositionToAttack && mpProjectileMasterStats)
      {
        SyVect3 aimLoc;
        float heading, distance;

        CalculateTargeting(aimLoc, heading, distance);
        GetIntel()->AttackRanged(heading, distance);
      }
    }
    else
    {
      GetIntel()->TurnTo(targetLoc);
    }
  }

  mLastFrameTargetLoc = targetLoc;
}

void cGoal_Ranged::CalculateTargeting(SyVect3& aimLoc, float& heading, float& distance)
{
  SyAssert(mpProjectileMasterStats!=NULL);

  SyVect3 targetLoc(GetTarget()->GetLocation()), targetVelocity(0.0f, 0.0f, 0.0f);
  float distToTarget = targetLoc.Distance(GetGameObject()->GetLocation());
  float timeToTarget = 1.0f;
  float accuracy = static_cast<cStatsCharacter*>(GetGameObject()->GetStats())->GetAccuracy();
  float projectileSpeed = mpProjectileMasterStats->mSpeed;
  float targetSpeed = mTargetAvgVelocity.Magnitude();

  if (projectileSpeed > 0.0f)
  {
    timeToTarget = distToTarget/projectileSpeed;
  }

  if (mpProjectileMasterStats->mGravity > 0.0f)
  {
    float deltaHeight = targetLoc.Y - GetGameObject()->GetLocation().Y;
    float yVel = deltaHeight/timeToTarget - timeToTarget * mpProjectileMasterStats->mGravity * cPhysics::GRAVITY * 0.5f;
    timeToTarget = -yVel/(mpProjectileMasterStats->mGravity * cPhysics::GRAVITY);
  }

//  AIDEBUGDRAW_LINE(GetGameObject()->GetID(), GetGameObject()->GetLocation()+SyVect3(0.0f, 1.0f, 0.0f), targetLoc+SyVect3(0.0f, 1.0f, 0.0f), cAIDebugDraw::CYAN);

  targetVelocity = mTargetAvgVelocity;
  targetVelocity *= timeToTarget + 0.5f; // include hack animation time
  targetLoc += targetVelocity;

  accuracy = SY_MAX(SY_MIN(accuracy, targetSpeed*3.0f), (accuracy<=0.1f?accuracy:0.1f));
  float sideError = (((float)(GetGameObject()->GetTitan()->Random(0, (int)(accuracy*200.0f))))/100.0f) - accuracy;
  float forwardError = (((float)(GetGameObject()->GetTitan()->Random(0, (int)(accuracy*200.0f))))/100.0f) - accuracy;

  // adjust distance by random error in direction towards target
  distance = targetLoc.Distance(GetGameObject()->GetLocation()) + forwardError;
  distance = SY_MAX(distance, 1.0f);

  // adjust heading by random arc length to side of target 
  // (angle covered = arclength/radius)
  heading = GetGameObject()->GetHeadingTowards(targetLoc) + (sideError/distance);

  aimLoc = GetGameObject()->GetLocation();
  aimLoc.X += SY_SIN(heading) * distance;
  aimLoc.Z += SY_COS(heading) * distance;

//  AIDEBUGDRAW_LINE(GetGameObject()->GetID(), GetGameObject()->GetLocation()+SyVect3(0.0f, 1.0f, 0.0f), aimLoc+SyVect3(0.0f, 1.0f, 0.0f), cAIDebugDraw::BLUE);
}

bool cGoal_Ranged::IsFriendlyInWay(const SyVect3& targetLoc, SyVect3& avoidPos)
{
  static const float ATTACK_FOV = SY_DEG_TO_RAD(10.0f);
  static const float MIN_ALIGNED_DOT = SY_COS(ATTACK_FOV);

  cGameObjectRegistry* pRegistry = GetGameObject()->GetRegistry();

  if (!pRegistry)
  {
    return false;
  }

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

  SyVect3 myLoc(GetGameObject()->GetLocation());
  SyVect3 toTarget(targetLoc-myLoc);
  SyVect3 toFriendly;
  float distToTarget = toTarget.NormalizeMagn();
  float distToFriendly;
  SyBBox friendlyBounds;
  bool bFoundFriendlyInWay = false;

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

    if (!pObj || !GetIntel()->IsFriendly(pObj))
    {
      continue;
    }

    toFriendly = pObj->GetLocation() - myLoc;
    distToFriendly = toFriendly.NormalizeMagn();

    if ((toFriendly ^ toTarget) >= MIN_ALIGNED_DOT &&
        distToFriendly <= distToTarget)
    {
      friendlyBounds.Bound(!bFoundFriendlyInWay, pObj->GetLocation());
      bFoundFriendlyInWay = true;
    }
  }

  if (bFoundFriendlyInWay)
  {
    toTarget.Normalize();
    SyVect3 right;
    right.Cross(toTarget, SyVect3(0.0f, 1.0f, 0.0f));
    right.Normalize();

    SyVect3 toCenter(friendlyBounds.Center() - myLoc), toCenterNorm;
    toCenterNorm = toCenter;
    toCenterNorm.Normalize();

    if ((toCenterNorm^right) >= 0.0f)
    {
      right = -right;
    }

    avoidPos = right;
    avoidPos *= 3.0f;
    avoidPos += myLoc;   
  }

  return bFoundFriendlyInWay;
}

bool cGoal_Ranged::MoveToAttackPosition(float speed)
{
  SyVect3 targetLoc(GetTarget()->GetLocation());
  SyVect3 lastSeenTargetLoc;

  float distToTarget = targetLoc.Distance(GetGameObject()->GetLocation());
  char behType = ((cStatsCharacter*)GetGameObject()->GetStats())->GetBehaviorType();

  bool bInPositionToAttack = true;

  if (NPCBEH_MELEERANGED != behType &&
      distToTarget < smFleeDistance)
  {
    bInPositionToAttack = false;
    if (!GetIntel()->IsAvoiding())
    {
      SyVect3 fleeDir;
      mpOwner->CalculateFleeDirection(GetTarget(), fleeDir);
      fleeDir *= smFleeDistance+2.0f;
      GetIntel()->GoTo(GetGameObject()->GetLocation()+fleeDir, 1.0f);
    }
  }
  else if ((!mpOwner->CanSeeAttackTarget() && mpOwner->GetLastSeenAttackTargetPos(lastSeenTargetLoc)) ||
           (mpOwner->CanSeeAttackTarget() && distToTarget > smMaxFiringDistance))
  {
    GetIntel()->GoTo(lastSeenTargetLoc, speed);
    bInPositionToAttack = false;
  }
  else
  {
    SyVect3 avoidPos;

    if (IsFriendlyInWay(targetLoc, avoidPos))
    {
      bInPositionToAttack = false;
      GetIntel()->Stop();

//      if (!GetIntel()->IsAvoiding())
//      {
//        GetIntel()->GoTo(avoidPos, speed);
//      }
    }
  }

  return bInPositionToAttack;
}

void cGoal_Ranged::RegisterTuningVariables()
{
  gTuningSys.AddFloat(&smFleeDistance, "AI_RangedCombat_FleeDistance");
  gTuningSys.AddFloat(&smMaxFiringDistance, "AI_RangedCombat_MaxFiringDistance");
}

//----------------------------------------------------------cGoal_RangedCircle
cGoal_RangedCircle::cGoal_RangedCircle(tGameObjectID target)
: cGoal_Ranged(target)
{
}

bool cGoal_RangedCircle::MoveToAttackPosition(float speed)
{
  static const float SEPARATED_RADIUS = 10.0f;
  static const float SEPARATED_TOLERANCE = 4.0f;
  bool bInAttackPosition = cGoal_Ranged::MoveToAttackPosition(speed);

  if (!bInAttackPosition)
  {
    return false;
  }

  // pick locations around the player considering attackers positions
  cAIBlackboard::Record rec;
  SyVect3 myLoc(GetGameObject()->GetLocation()), meToAttacker;
  SyVect3 targetLoc(GetTarget()->GetLocation()), targetToAttacker;
  SyVect3 targetDir(myLoc-targetLoc), rightTargetDir;
  cGameObject* pAttacker;
  cGameObject* pRightClosestAttacker = NULL;
  cGameObject* pLeftClosestAttacker = NULL;
  float leftClosestDist = -SEPARATED_RADIUS;
  float rightClosestDist = SEPARATED_RADIUS;
  float proj;
  int numAttackers = 0;

  float myDistToTarget = targetDir.NormalizeMagn();
  rightTargetDir.Cross(targetDir, SyVect3(0.0f, 1.0f, 0.f));
  rightTargetDir.Normalize();

  // find attackers of same target on either side of me
  if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_ATTACKING_RANGED, ID_NONE, mTargetID, rec))
  { 
    do
    {
      pAttacker = GetRegistry()->Fetch(rec.mSourceID);        
      if (pAttacker != GetGameObject())
      {
        if (myLoc.DistanceSquared(pAttacker->GetLocation()) < SEPARATED_RADIUS*SEPARATED_RADIUS*8.0f)
        {
          targetToAttacker = pAttacker->GetLocation()-targetLoc;

          if ((targetToAttacker^targetDir) > -0.8f)
          {
            ++numAttackers;

            meToAttacker = pAttacker->GetLocation()-myLoc;
            proj = rightTargetDir^meToAttacker;

            if (proj < 0.0f && proj > leftClosestDist)
            {
              pLeftClosestAttacker = pAttacker;
              leftClosestDist = proj;
            }
            else if (proj > 0.0f && proj < rightClosestDist)
            {
              pRightClosestAttacker = pAttacker;
              rightClosestDist = proj;
            }
          }
        }
      }
    }
    while (cAIBlackboard::Get()->GetNextRecord(rec));
  }

  if (numAttackers > 0)
  {
    // move to keep between two neighbors
    SyVect3 leftAdjust(0.0f, 0.0f, 0.0f), rightAdjust(0.0f, 0.0f, 0.0f), avoid(0.0f, 0.0f, 0.0f);
    float maxAttackerAdjust = ((SEPARATED_RADIUS*0.5f*SY_PI)/((float)numAttackers));

    if (pLeftClosestAttacker && -leftClosestDist < maxAttackerAdjust)
    {
      leftAdjust = rightTargetDir;
      leftAdjust *= maxAttackerAdjust+leftClosestDist;
      AIDEBUGDRAW_LINE(GetGameObject()->GetID(), myLoc+SyVect3(0.0f, 2.0f, 0.0f), myLoc+leftAdjust+SyVect3(0.0f, 2.0f, 0.0f), cAIDebugDraw::BLUE);
    }

    if (pRightClosestAttacker && rightClosestDist < maxAttackerAdjust)
    {
      rightAdjust = -rightTargetDir;
      rightAdjust *= maxAttackerAdjust-rightClosestDist;
      AIDEBUGDRAW_LINE(GetGameObject()->GetID(), myLoc+SyVect3(0.0f, 2.0f, 0.0f), myLoc+rightAdjust+SyVect3(0.0f, 2.0f, 0.0f), cAIDebugDraw::YELLOW);
    }

    // also move to stay within desired radius of the target
    if ((myDistToTarget < SEPARATED_RADIUS-SEPARATED_TOLERANCE) ||
        (myDistToTarget > SEPARATED_RADIUS+SEPARATED_TOLERANCE))
    {
      avoid = targetDir;
      avoid *= SEPARATED_RADIUS-myDistToTarget;
      AIDEBUGDRAW_LINE(GetGameObject()->GetID(), myLoc+SyVect3(0.0f, 2.0f, 0.0f), myLoc+avoid+SyVect3(0.0f, 2.0f, 0.0f), cAIDebugDraw::RED);
    }

    avoid += leftAdjust;
    avoid += rightAdjust;

    if (avoid.Magnitude() > SEPARATED_TOLERANCE)
    {
      GetIntel()->GoTo(myLoc+avoid, speed);
      return false;
    }
  }

  return true;
}

//----------------------------------------------------------cGoal_RangedLine
cGoal_RangedLine::cGoal_RangedLine(tGameObjectID target)
: cGoal_Ranged(target)
{
}

void cGoal_RangedLine::AttackEnter()
{
  cGameObject* pTarget = GetTarget();

  if (pTarget)
  {
    cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_RANGEDATTACK_LINE, GetGameObject()->GetID(), pTarget->GetID());
  }

  cGoal_Ranged::AttackEnter();
}

void cGoal_RangedLine::AttackExit()
{
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_RANGEDATTACK_LINE, GetGameObject()->GetID());
  cGoal_Ranged::AttackExit();
}

bool cGoal_RangedLine::MoveToAttackPosition(float speed)
{
  static const float MINIMUM_FORWARD_DISTANCE = 8.0f;
  static const float MINIMUM_SIDEWAYS_DISTANCE = 4.0f;
  static const float SIDEWAYS_TOLERANCE = 1.0f;
  static const float FORWARD_TOLERANCE = 2.0f;

  bool bInAttackPosition = cGoal_Ranged::MoveToAttackPosition(speed);

  if (!bInAttackPosition)
  {
    return false;
  }

  // pick locations around the player considering attackers positions
  cAIBlackboard::Record rec;
  SyVect3 myLoc(GetGameObject()->GetLocation()), meToAttacker;
  SyVect3 targetLoc(GetTarget()->GetLocation());
  SyVect3 meToTargetDir(targetLoc-myLoc), rightTargetDir;
  SyVect3 attackerToTarget, attackerLoc, attackCenter(0.0f, 0.0f, 0.0f);
  cGameObject* pAttacker;
  cGameObject* pClosestAttacker = NULL;
  float dist, closestDist = 0.0f;
  int numAttackers = 0;

  meToTargetDir.NormalizeMagn();
  rightTargetDir.Cross(meToTargetDir, SyVect3(0.0f, 1.0f, 0.f));
  rightTargetDir.Normalize();

  // find attackers of same target on either side of me
  if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_RANGEDATTACK_LINE, ID_NONE, mTargetID, rec))
  { 
    do
    {
      pAttacker = GetRegistry()->Fetch(rec.mSourceID);        
      if (pAttacker != GetGameObject())
      {
        attackerLoc = pAttacker->GetLocation();
        if (((targetLoc - attackerLoc) ^ meToTargetDir) > 0.0f)
        {
          attackCenter += attackerLoc;
          ++numAttackers;

          dist = (attackerLoc-myLoc).Magnitude();
          if (!pClosestAttacker || dist < closestDist)
          {
            pClosestAttacker = pAttacker;
            closestDist = dist;
          }
        }
      }
    }
    while (cAIBlackboard::Get()->GetNextRecord(rec));
  }

  if (numAttackers > 0)
  {
    attackCenter /= (float) numAttackers;

    bool bAdjust = false;
    SyVect3 forwardAdjust(0.0f, 0.0f, 0.0f), sidewaysAdjust(0.0f, 0.0f, 0.0f);
    SyVect3 meToAttackCenter(attackCenter-myLoc);
    SyVect3 targetToAttackCenterDir(attackCenter-targetLoc);
    float distTargetToCenter = targetToAttackCenterDir.NormalizeMagn();

    if (distTargetToCenter < MINIMUM_FORWARD_DISTANCE)
    {
      SyVect3 moveAttackCenter(targetToAttackCenterDir);
      moveAttackCenter *= MINIMUM_FORWARD_DISTANCE-distTargetToCenter;
      attackCenter += moveAttackCenter;
      meToAttackCenter = attackCenter-myLoc;
      distTargetToCenter = MINIMUM_FORWARD_DISTANCE;
    }

    float projMeToCenter = meToAttackCenter^targetToAttackCenterDir;
    if ((projMeToCenter < 0.0f && projMeToCenter < -FORWARD_TOLERANCE) ||
        (projMeToCenter > 0.0f && projMeToCenter > FORWARD_TOLERANCE))
    {
      bAdjust = true;
      forwardAdjust = targetToAttackCenterDir;
      forwardAdjust *= projMeToCenter + (projMeToCenter>0.0f?+FORWARD_TOLERANCE:0.0f);
      AIDEBUGDRAW_LINE(GetGameObject()->GetID(), myLoc+SyVect3(0.0f, 3.0f, 0.0f), myLoc+forwardAdjust+SyVect3(0.0f, 3.0f, 0.0f), cAIDebugDraw::CYAN);
    }

    if (pClosestAttacker)
    {
      float proj = (pClosestAttacker->GetLocation()-myLoc)^rightTargetDir;

      if (SY_FABS(proj) < MINIMUM_SIDEWAYS_DISTANCE - SIDEWAYS_TOLERANCE)
      {
        bAdjust = true;
        sidewaysAdjust = rightTargetDir;
      
        if (proj > 0.0f)
        {
          sidewaysAdjust = -sidewaysAdjust;
          proj = -proj;
        }

        sidewaysAdjust *= MINIMUM_SIDEWAYS_DISTANCE + SIDEWAYS_TOLERANCE + proj;
        AIDEBUGDRAW_LINE(GetGameObject()->GetID(), myLoc+SyVect3(0.0f, 3.0f, 0.0f), myLoc+sidewaysAdjust+SyVect3(0.0f, 3.0f, 0.0f), ((pClosestAttacker->GetLocation()-myLoc)^rightTargetDir)> 0.0f?cAIDebugDraw::MAGENTA:cAIDebugDraw::YELLOW);
      }
    }

    if (bAdjust && !GetIntel()->IsAvoiding())
    {
      GetIntel()->GoTo(myLoc+forwardAdjust+sidewaysAdjust, speed);
      return false;
    }
  }

  return true;
}

//----------------------------------------------------------cGoal_RangedStun
cGoal_RangedStun::cGoal_RangedStun(tGameObjectID target)
: cGoal_Ranged(target)
{
}

void cGoal_RangedStun::AttackExit()
{
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_ATTACKING_MELEE, GetGameObject()->GetID());
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_STUNNING, GetGameObject()->GetID());
  cGoal_Ranged::AttackExit();
}

void cGoal_RangedStun::Update(float time)
{
  cGameObject* pTarget = GetTarget();
  bool bTargetStunned = false;

  if (pTarget)
  {
    SyAssertf(prop_cast<cStatsCharacter*>(pTarget->GetStats())!=NULL, "RangedStun goal expects a character target");
    cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pTarget->GetStats());
    bTargetStunned = pStats->HasCondition("Stunned");
  }

  if (bTargetStunned && FindTarget(false))
  {
    // stop telling everyone we're stunning the target
    // (since now we're just gonna beat him up).
    cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_STUNNING, GetGameObject()->GetID(), mTargetID);

    if (IsAttackDone() || mpOwner->IsOutsideDefendRadius())
    {
      mpOwner->Pop(this);
      return;
    }

    bool bCanAttack = CheckAttackDelay(time);

    float speed = ((cStatsCharacter*)GetGameObject()->GetStats())->GetMovementSpeed();
    float distToTarget = GetGameObject()->GetDistance(pTarget);
    float attackRange = GetAnimInterface()->GetRange(COMBO_L);
    if (distToTarget < attackRange && bCanAttack)
    {
      cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_ATTACKING_MELEE, GetGameObject()->GetID(), mTargetID, 0, GetGameObject()->GetLocation());
      GetIntel()->AttackMelee(GetTarget(), attackRange);
    }
    else
    {
      GetIntel()->GoTo(pTarget->GetLocation(), speed);
    }

    mLastFrameTargetLoc = GetTarget()->GetLocation();
  }
  else
  {
    // we're done melee attacking
    cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_ATTACKING_MELEE, GetGameObject()->GetID());

    // we've started attempting to stun the target
    cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_STUNNING, GetGameObject()->GetID(), mTargetID);

    cGoal_Ranged::Update(time);
  }
}

//----------------------------------------------------------cGoal_RangedTeleport
cGoal_RangedTeleport::cGoal_RangedTeleport(tGameObjectID target)
: cGoal_Ranged(target),
  mbFoundTeleport(false),
  mTeleportHeading(0.0f),
  mTeleportTimer(0.0f),
  mTeleportTimeRandAdjust(0.0f)
{
}

void cGoal_RangedTeleport::AttackEnter()
{
  cGoal_Ranged::AttackEnter();
  mTeleportHeading = SY_DEG_TO_RAD((float)(GetGameObject()->GetTitan()->Random(0, 359)));
  mTeleportTimeRandAdjust = (float)(GetGameObject()->GetTitan()->Random(0, 2000))*0.001f;
}

void cGoal_RangedTeleport::Update(float time)
{
  cGoal_Ranged::Update(time);

  mTeleportTimer += time;

  static const float RANGED_TELEPORT_TIME = 7.0f;
  static const float RANGED_TELEPORT_DISTANCE = 8.0f;
  static const float RANGED_TELEPORT_HEADING_INTERVAL = SY_DEG_TO_RAD(11.0f); // must not be divisible by 360

  if (!mbFoundTeleport)
  {
    mLOSTarget.Update(GetGameObject(), time);
    
    if (mLOSTarget.CanSeeTarget())
    {
      mbFoundTeleport = true;
    }
    else if (!mLOSTarget.IsLOSPending())
    {
      mTeleportHeading += RANGED_TELEPORT_HEADING_INTERVAL;

      SyVect3 testPos = GetGameObject()->GetLocation();
      testPos.X += SY_SIN(mTeleportHeading) * RANGED_TELEPORT_DISTANCE;
      testPos.Z += SY_COS(mTeleportHeading) * RANGED_TELEPORT_DISTANCE;

      SyCollRay ray;
      SySceneFilter filter;
      ray.Init(testPos+SyVect3(0.0f, 1.0f, 0.0f), testPos+SyVect3(0.0f, -5.0f, 0.0f));
      filter.Init();
      if (GetGameObject()->GetTitan()->GetScene()->Collide(ray, filter) &&
          ray.GetHitPoint().Y < 0.5f)
      {
        mLOSTarget.SetTargetPosition(ray.GetHitPoint()+SyVect3(0.0f, 1.0f, 0.0f));
      }
    }
  }

  if (mbFoundTeleport)
  {
    if ((mTeleportTimer > RANGED_TELEPORT_TIME+mTeleportTimeRandAdjust) ||
        (GetGameObject()->GetDistance(GetTarget()) <= cGoal_Ranged::GetFleeDistance()))
    {
      GetGameObject()->SetLocation(mLOSTarget.mTargetPos);
      GetIntel()->TurnTo(GetTarget()->GetLocation());
      mbFoundTeleport = false;
      mTeleportTimer = 0.0f;
      mTeleportHeading = SY_DEG_TO_RAD((float)(GetGameObject()->GetTitan()->Random(0, 359)));
      mLOSTarget.Clear();
    }
  }
}


// EOF
