/******************************************************************
  
  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 "../spell.h"
#include "../debugoverlay.h"
#include "../gameerror.h"

#include "behaviortypes.h"
#include "aiblackboard.h"

#include <algorithm>
//---------------------------------------------- Class Declarations

//------------------------------------------- cGoal_Attack
int cGoal_Attack::smMaxMeleeAttackers = 3;
float cGoal_Attack::smMaxRangedFiringDistance = 50.0f;

cGoal_Attack::cGoal_Attack(tGameObjectID target)
: cAiGoal(),
  mTargetID(target),
  mPrevAnimState(NUM_ANIM_STATES),
  mNumAttacks(0),
  mAttackDelayTimer(0.0f),
  mbInsideCloseDist(false),
  mbInsideMeleeRange(false),
  mCurAttackAbilityID(ID_NONE),
  mMoveTimer(0.0f),
  mbUseMoveTimer(false),
  mbBreakTargetBlock(false),
  mLastFrameTargetLoc(0.0f, 0.0f, 0.0f),
  mTargetAvgVelocity(0.0f, 0.0f, 0.0f),
  mTargetExplodingPropID(ID_NONE)
{
}

void cGoal_Attack::Enter()
{
  mCurAttackAbilityID = ID_NONE;
  mPrevAnimState = NUM_ANIM_STATES;
  mNumAttacks = 0;
  mAttackDelayTimer = 0.0f;
  mbInsideCloseDist = false;
  mbInsideMeleeRange = false;
  mMoveTimer = 0.0f;
  mbUseMoveTimer = false;

  cGameObject* pTarget = GetTarget();

  if (pTarget)
  {
    mLastFrameTargetLoc = pTarget->GetLocation();
  }
  else
  {
    mLastFrameTargetLoc(0.0f, 0.0f, 0.0f);
  }

  cGameObject* pObj = GetGameObject();
  cStatsCharacter *pStats = static_cast<cStatsCharacter*>(pObj->GetStats());

  if (!mpOwner->IsRangedAttacker() &&
      pTarget &&
      pObj->GetDistance(pTarget) >= pStats->GetAIMeleeRange())
  {
    mbInsideMeleeRange = true;
  }

  // do subclass tasks now
  AttackEnter();

  if (mbInsideMeleeRange)
  {
    MeleeEnter();
    EquipItem(EQUIP_MELEE);
  }
  else
  {
    RangedEnter();
    EquipItem(EQUIP_RANGED);
  }
}

void cGoal_Attack::Exit()
{
  if (mbInsideMeleeRange)
  {
    MeleeExit();
  }
  else
  {
    RangedExit();
  }


  AttackExit();

  GetIntel()->Block(0.0f);

  GetAnimInterface()->GetInput()->mbKeepFacing = false;

  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_ATTACKING_MELEE, mpOwner->GetOwner()->GetID());
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_ATTACKING_RANGED, GetGameObject()->GetID());
}

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

void 
cGoal_Attack::RefreshTargeting()
{
  // update targeting while playing animation (continual aiming)
  cAnimCharControllerInterface* pAnimIFace = GetAnimInterface();
  if (pAnimIFace->GetAnimState() == AS_ATTACK_RANGED && mTargetExplodingPropID == ID_NONE)
  {
    SyVect3 aimLoc;
    float heading, pitch, distance;
    CalculateRangedTargeting(aimLoc, heading, pitch, distance, false);
    pAnimIFace->RefreshTargeting(heading, pitch, distance);
  }
  else if (pAnimIFace->IsCasting() && ID_NONE != mCurAttackAbilityID)
  {
    cGameObject* pObj = GetGameObject();
    cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());

    const cAbilityMaster* pAbility = pStats->GetAbility(mCurAttackAbilityID);
    SyAssert(pAbility);
    
    if (pAbility && ID_NONE != pAbility->mSpell)
    {
      const cSpellMaster* pSpell = pObj->GetTitan()->GetDatabaseSys()->GetSpellMaster(pAbility->mSpell);
      SyAssert(pSpell);

      if (pSpell)
      {
        if (ID_NONE != pSpell->mProjectileTypeID ||
            cSpellMaster::SPELL_ORIGIN_TARGET_LOCATION == pSpell->mOrigin ||
            cSpellMaster::SPELL_ORIGIN_TARGET == pSpell->mOrigin )
        {
          SyVect3 aimLoc;
          float heading, pitch, distance;
          CalculateRangedTargeting(aimLoc, heading, pitch, distance, false);
          pAnimIFace->RefreshTargeting(heading, pitch, distance);
        }
      }
    }
  }
}

void cGoal_Attack::BreakTargetBlock()
{
  cStatsCharacter *pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  const cStatsAbilitySet* pAbilitySet = pStats->GetAbilitySet();

  if (!pAbilitySet)
  {
    return;
  }

  tGameID blockBreakAbility = ID_NONE;

  if (pAbilitySet->GetAbility(cStatsAbilitySet::ABILITY_CONDITION_BLOCK_BREAK, ID_NONE, &blockBreakAbility))
  {
    GAME_ASSERT(ERROR_CODE, blockBreakAbility != ID_NONE, "Could not find block break ability");
    mCurAttackAbilityID = blockBreakAbility;
    mbBreakTargetBlock = true;
  }
}

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

  cGameObject* pObj = GetGameObject();
  SyAssert(prop_cast<cStatsCharacter*>(pObj->GetStats())!=NULL);
  cStatsCharacter *pStats = static_cast<cStatsCharacter*>(pObj->GetStats());
  const cStatsBlockBehavior* pBlockBehavior = pStats->GetBlockBehavior();
  const cStatsAbilitySet* pAbilitySet = pStats->GetAbilitySet();
  cAnimCharControllerInterface *pAnimCtrlr = GetAnimInterface();

  float attackDelay = 0.0f;

  if (pAbilitySet && ID_NONE != mCurAttackAbilityID)
  {
    pAbilitySet->GetDelay(mCurAttackAbilityID, &attackDelay);
  }

  bool bIsAttacking = pAnimCtrlr->IsAttacking();
  bool bIsComboing = false;

  if (mPrevAnimState != pAnimCtrlr->GetAnimState() && bIsAttacking)
  {
    ++mNumAttacks;
  }

  mPrevAnimState = pAnimCtrlr->GetAnimState();

  if (bIsAttacking)
  {
    tGameID nextAbility;
    if (pAbilitySet && pAbilitySet->GetNextAbility(mCurAttackAbilityID, &nextAbility))
    {
      mAttackDelayTimer = 0.0f;
      bIsComboing = true;
    }
    else
    {
      mAttackDelayTimer = attackDelay;
      bCanAttack = false;
    }
  }
  else
  {
    if (mAttackDelayTimer > 0.0f && mAttackDelayTimer <= time)
    {
      mAttackDelayTimer = 0.0f;
      mNumAttacks = 0;
      bCanAttack = false;

      if (pBlockBehavior && cStatsBlockBehavior::NPCBS_AFTER_ATTACK == pBlockBehavior->mNPCBlockStrategy)
      {
        mpOwner->BlockOpportunity(cStatsBlockBehavior::BLOCK_ATTACK_LIGHT);
      }
    }
    else if (mAttackDelayTimer > time)
    {
      mAttackDelayTimer -= time;
      bCanAttack = false;
    }
  }

  if (!bIsComboing && pObj->GetTitan()->IsGlobalAIAttackTimerActive())
  {
    bCanAttack = false;
  }

  if (bCanAttack)
  {
    SyAssert(prop_cast<cGraphicCharacter*>(GetTarget()->GetGraphic()));
    cAnimCharControllerInterface* pTargetAnimCtrlr = static_cast<cGraphicCharacter*>(GetTarget()->GetGraphic())->GetAnimController();
    if (pTargetAnimCtrlr && pTargetAnimCtrlr->IsGettingUp())
    {
      bCanAttack = false;
    }
  }

  return bCanAttack;
}


bool cGoal_Attack::CheckBlockOpportunity(float time, bool bInsideCloseDist)
{
  cAnimCharControllerInterface* pAnimCtrlr = GetAnimInterface();
  bool bIsBlocking = pAnimCtrlr->IsBlocking() || pAnimCtrlr->IsDodging();

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

  cStatsCharacter *pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  const cStatsBlockBehavior* pBlockBehavior = pStats->GetBlockBehavior();

  if (!mbInsideCloseDist &&
      bInsideCloseDist &&
      (cStatsBlockBehavior::NPCBS_ON_CLOSE == pBlockBehavior->mNPCBlockStrategy ||
       cStatsBlockBehavior::NPCBS_WHILE_CLOSE == pBlockBehavior->mNPCBlockStrategy))
  {
    bIsBlocking = mpOwner->BlockOpportunity(cStatsBlockBehavior::BLOCK_ATTACK_LIGHT);
  }
  else if (bInsideCloseDist &&
           cStatsBlockBehavior::NPCBS_WHILE_CLOSE == pBlockBehavior->mNPCBlockStrategy)
  {
    if (pAnimCtrlr->IsBlocking() && pAnimCtrlr->GetInput()->mBlockRequest)
    {
      bIsBlocking = true;
      GetIntel()->Block(pBlockBehavior->mBlockHoldTime);
    }
    else if (pBlockBehavior->mRipostePercentChance > 0)
    {
      SyAssert(prop_cast<cGraphicCharacter*>(GetTarget()->GetGraphic()));
      cAnimCharControllerInterface* pTargetAnimCtrlr = static_cast<cGraphicCharacter*>(GetTarget()->GetGraphic())->GetAnimController();
      if (!(pTargetAnimCtrlr->IsAttacking() && !pTargetAnimCtrlr->IsAnimStateActive()))
      {
        bIsBlocking = mpOwner->BlockOpportunity(cStatsBlockBehavior::BLOCK_ATTACK_LIGHT);
      }
    }
  }

  mbInsideCloseDist = bInsideCloseDist;

  return bIsBlocking;
}

bool cGoal_Attack::IsAttackDone()
{

  if (ID_NONE == mTargetID)
  {
    return true;
  }
  
  cGameObject* pTarget = GetTarget();
  
  if (pTarget == NULL || 
      pTarget->GetStats()->IsDead() || 
      mpOwner->GetOwner()->GetStats()->IsDead() ||
      pTarget != mpOwner->GetAttackTarget())
  {
    return true;
  }

  return false;
}

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

  GAME_ASSERT(ERROR_CODE, mpOwner->GetAttackTarget() == NULL || mpOwner->GetAttackTarget() == pTarget, "cGoal_Attack::FindTarget has invalid target");

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

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

  GAME_ASSERT(ERROR_CODE, pTarget->GetType()==cGameObject::OBJ_NPC || pTarget->GetType() == cGameObject::OBJ_PLAYER, "cGoal_Attack::FindTarget expects a character as a target");

  if (static_cast<cStatsCharacter*>(GetGameObject()->GetStats())->GetAIBehaviorType() == NPCBEH_SENTINEL)
  {
    bHuntDown = false;
  }

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

    return false;
  }

  return true;
}

void cGoal_Attack::AttackMelee()
{
  cGameObject* pObj = GetGameObject();
  cGraphicCharacter* pGraphic = static_cast<cGraphicCharacter*>(pObj->GetGraphic());
  eComboType curCombo = pGraphic->GetAnimController()->GetComboType();
  eComboType nextCombo = COMBO_L;

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());
  const cStatsAbilitySet* pAbilitySet = pStats->GetAbilitySet();
  const cAbilityMaster* pCurAbility = NULL;
  const cAbilityMaster* pNextAbility = NULL;

  if (ID_NONE != mCurAttackAbilityID)
  {
    pCurAbility = pStats->GetAbility(mCurAttackAbilityID);
  }

  if (pAbilitySet)
  {  
    pNextAbility = NULL;
    if (mbBreakTargetBlock && ID_NONE != mCurAttackAbilityID)
    {
      mbBreakTargetBlock = false;
      pNextAbility = pStats->GetAbility(mCurAttackAbilityID);
      GAME_ASSERT(ERROR_CODE, pNextAbility!=NULL, "Could not find attack ability for NPC");
    }
    else if (!pCurAbility ||
             NUM_COMBOS == curCombo ||
             curCombo == pCurAbility->mCombo)
    {
      tGameID curAbilityID = mCurAttackAbilityID;
      tGameID nextAbilityID = ID_NONE;
      mCurAttackAbilityID = ID_NONE;

      if (pAbilitySet->GetAbility(cStatsAbilitySet::ABILITY_CONDITION_MELEE_ATTACK, curAbilityID, &nextAbilityID))
      {
        SyAssert(ID_NONE != nextAbilityID);
        pNextAbility = pStats->GetAbility(nextAbilityID);
        GAME_ASSERT(ERROR_CODE, pNextAbility!=NULL, "Could not find attack ability for NPC");
      }
    }
    else if (pCurAbility)
    {
      pNextAbility = pCurAbility;
    }

    if (pNextAbility)
    {
      mCurAttackAbilityID = pNextAbility->mID.GetID();
      nextCombo = pNextAbility->mCombo;
    }
  }

  if (NUM_COMBOS == nextCombo)
  {
    SyAssert(pNextAbility!=NULL && pNextAbility->mSpell!=ID_NONE);
    
    const cSpellMaster* pSpell = pObj->GetTitan()->GetDatabaseSys()->GetSpellMaster(pNextAbility->mSpell);
    if (pSpell)
    {
      pSpell->CastSpell(pObj, GetTarget());
      pAbilitySet->GetDelay(mCurAttackAbilityID, &mAttackDelayTimer);
    }
  }
  else
  {
    if (!GetIntel()->AttackMelee(GetTarget(), nextCombo))
    {
      mCurAttackAbilityID = ID_NONE;
    }
  }
}


void cGoal_Attack::AttackRanged(float heading, float pitch, float distance)
{
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cDatabaseSys* pDB = GetGameObject()->GetTitan()->GetDatabaseSys();
  const cStatsAbilitySet* pAbilitySet = pStats->GetAbilitySet();
  const cAbilityMaster* pRangedAbility = NULL;

  if (pAbilitySet)
  {  
    tGameID curAbilityID = mCurAttackAbilityID;
    tGameID nextAbilityID = ID_NONE;
    mCurAttackAbilityID = ID_NONE;

    if (pAbilitySet->GetAbility(cStatsAbilitySet::ABILITY_CONDITION_RANGED_ATTACK, curAbilityID, &nextAbilityID))
    {
      SyAssert(ID_NONE != nextAbilityID);

      pRangedAbility = pStats->GetAbility(nextAbilityID);
      GAME_ASSERT(ERROR_CODE, pRangedAbility!=NULL, "Could not find ranged attack ability for NPC with character master %s", pStats->GetMasterName());
    }
  }

  if (pRangedAbility) 
  {
    mCurAttackAbilityID = pRangedAbility->mID.GetID();

    if (ID_NONE != pRangedAbility->mSpell)
    {
      const cSpellMaster* pSpell = pDB->GetSpellMaster(pRangedAbility->mSpell);
      if (pSpell)
      {
        pSpell->CastSpell(GetGameObject(), GetTarget());
        pAbilitySet->GetDelay(mCurAttackAbilityID, &mAttackDelayTimer);
        return;
      }
    }
  }

  GetIntel()->AttackRanged(heading, pitch, distance);
}

void cGoal_Attack::EquipItem(eEquipSlot slot)
{
  cInventoryCharacter *pInv = static_cast<cInventoryCharacter*>(GetGameObject()->GetInventory());
  SyAssert(pInv!=NULL);

  if (!pInv || pInv->GetEquippedItem(slot))
  {
    return;
  }

  cItem* pItem;
  for (int i=0; i<pInv->GetNumItems(); ++i)
  {
    pItem = pInv->GetItem(i);
    if (pItem && pItem->CanEquip(slot))
    {
      pInv->Equip(pItem, slot);
      return;
    }
  }
}

bool cGoal_Attack::IsAllyInWay(const SyVect3& targetLoc,
                               SyVect3& avoidPos)
{
  static const float MIN_FRIENDLY_TEST_FOV = SY_DEG_TO_RAD(12.5f);
  static const float MAX_FRIENDLY_TEST_FOV = SY_DEG_TO_RAD(20.0f);
  static const float FRIENDLY_TEST_NEAR_DIST = 3.0f;
  static const float FRIENDLY_TEST_FAR_DIST = 10.0f;
  static const float TEST_DIST_PAST_TARGET = 1.0f;

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

  if (!pRegistry)
  {
    return false;
  }

  SyVect3 myLoc(pOwner->GetLocation());
  SyVect3 toTarget(targetLoc-myLoc);
  SyVect3 toFriendly;
  float distToTarget = toTarget.NormalizeMagn();
  float distToFriendly;
  SyBBox friendlyBounds;
  bool bFoundFriendlyInWay = false;
  float projectileSpeed, projectileGravity;
  bool bProjectileIgnoresAllies;
  GetRangedAttack(projectileSpeed, projectileGravity, bProjectileIgnoresAllies);

  if (bProjectileIgnoresAllies || projectileGravity > 3.0f)
  {
    return false;
  }

  float testAngle = MIN_FRIENDLY_TEST_FOV; // if target within near distance, test min angle

  if (distToTarget >= FRIENDLY_TEST_NEAR_DIST &&
      distToTarget <= FRIENDLY_TEST_FAR_DIST)
  {
    // target in between near and far test distance, test average angle
    testAngle = (((distToTarget-FRIENDLY_TEST_NEAR_DIST)/(FRIENDLY_TEST_FAR_DIST-FRIENDLY_TEST_NEAR_DIST)) * (MAX_FRIENDLY_TEST_FOV-MIN_FRIENDLY_TEST_FOV)) + MIN_FRIENDLY_TEST_FOV;
  }
  else if (distToTarget > FRIENDLY_TEST_FAR_DIST)
  {
    testAngle = MAX_FRIENDLY_TEST_FOV; // target past far distance, test full angle
  }

  float testCos = SY_COS(testAngle);

  cGameObject* pObj = pRegistry->BeginType(cGameObject::OBJ_NPC);
  for(;pObj != NULL;pObj= pRegistry->NextType(pObj))
  {
    if (!pObj || pObj == pOwner || !GetIntel()->IsFriendly(pObj))
    {
      continue;
    }

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

    if (distToFriendly <= distToTarget + TEST_DIST_PAST_TARGET)
    {
      if ((toFriendly ^ toTarget) >= testCos)
      {
        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_Attack::GetRangedAttack(float& speed, float& gravMult, bool& bProjectileIgnoresAllies)
{
  speed = 10.0f;
  gravMult = 0.0f;
  bProjectileIgnoresAllies = false;

  tGameID projID = ID_NONE;
  cGameObject* pObj = GetGameObject();

  if (ID_NONE != mCurAttackAbilityID)
  {
    const cAbilityMaster* pAbility = static_cast<cStatsCharacter*>(pObj->GetStats())->GetAbility(mCurAttackAbilityID);

    if (pAbility && ID_NONE != pAbility->mSpell)
    {
      const cSpellMaster* pSpell = pObj->GetTitan()->GetDatabaseSys()->GetSpellMaster(pAbility->mSpell);

      if (pSpell)
      {
        projID = pSpell->mProjectileTypeID;
        bProjectileIgnoresAllies = (cSpellMaster::SPELL_TARGETING_ENEMIES == pSpell->mTargeting) || (cSpellMaster::SPELL_TARGETING_ENEMIES_AND_PROPS == pSpell->mTargeting);
      }
    }
  }

  if (ID_NONE == projID)
  {
    cItem* pItem = static_cast<cInventoryCharacter*>(pObj->GetInventory())->GetEquippedItem(EQUIP_RANGED);
    if (pItem)
    {
      projID = pItem->GetMaster()->mProjectile;
    }
  }

  if (ID_NONE == projID)
  {
    const cStatsItemMaster* pItemMaster = pObj->GetTitan()->GetDatabaseSys()->GetItemMaster(static_cast<cStatsCharacter*>(pObj->GetStats())->GetMaster()->mNaturalRanged);

    if (pItemMaster)
    {
      projID = pItemMaster->mProjectile;
    }
  }

  const cStatsProjectileMaster* pProjMaster = pObj->GetTitan()->GetDatabaseSys()->GetProjectileMaster(projID);

  if (pProjMaster)
  {
    speed = pProjMaster->mSpeed;
    gravMult = pProjMaster->mGravity;
  }

  return true;
}

void cGoal_Attack::CalculateRangedTargeting(SyVect3& aimLoc,
                                            float& heading,
                                            float& pitch,
                                            float& distance,
                                            bool bFindExplodingProps)
{
  cGameObject* pMyObj = GetGameObject();
  cGameObject* pTarget = GetTarget();
  SyVect3 targetLoc(pTarget->GetLocation()), targetVelocity(0.0f, 0.0f, 0.0f);
  float distToTarget = targetLoc.Distance(pMyObj->GetLocation());
  float timeToTarget = 1.0f;
  float accuracy = static_cast<cStatsCharacter*>(pMyObj->GetStats())->GetAIAccuracy();
  float projectileSpeed;
  float gravityMult;
  float targetSpeed = mTargetAvgVelocity.Magnitude();
  bool bProjectileIgnoresAllies;

  GetRangedAttack(projectileSpeed, gravityMult, bProjectileIgnoresAllies);

  // see if we can target exploding barrels nearby the target
  static const float TARGET_EXPLODING_PROP_CHANCE = 30;

  if (bFindExplodingProps && pMyObj->GetTitan()->Random(1, 100) <= TARGET_EXPLODING_PROP_CHANCE)
  {
    static const tGameID EXPLODING_PROP_ID = SyHashResourceID("ExplodingBarrel");
    static const float EXPLOSION_DISTANCE = 4.0f;
    cGameObjectRegistry* pRegistry = pMyObj->GetRegistry();
    float distToProp;

    cGameObject* pProp = pRegistry->BeginType(cGameObject::OBJ_PROP);
    for(; pProp != NULL; pProp = pRegistry->NextType(pProp))
    {
      SyAssert(pProp->GetType() == cGameObject::OBJ_PROP);
      if (!pProp ||
        static_cast<cStatsProp*>(pProp->GetStats())->GetMaster()->mID.GetID() != EXPLODING_PROP_ID ||
        pProp->GetStats()->IsDead())
      {
        continue;
      }

      distToProp = pProp->GetDistance(pTarget);

      if ((distToProp <= EXPLOSION_DISTANCE) &&
        (pProp->GetDistance(pMyObj) > EXPLOSION_DISTANCE))
      {
        // BAMMO! Blow up the barrel next to the target
        aimLoc = pProp->GetLocation();
        heading = pMyObj->GetHeadingTowards(aimLoc);
        pitch = pMyObj->GetPitchTowards(aimLoc);
        distance = distToProp;
        aimLoc.Y += 0.5f;
        mTargetExplodingPropID = pProp->GetID();
        return;
      }
    }
  }

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

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

  //  DEBUGOVERLAY_DRAWLINE(GetGameObject()->GetID(), "AI", GetGameObject()->GetLocation()+SyVect3(0.0f, 1.0f, 0.0f), targetLoc+SyVect3(0.0f, 1.0f, 0.0f), cDebugOverlay::CYAN);

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

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

  SyVect3 horiz(targetLoc - pMyObj->GetLocation());
  horiz.Y = 0.0f;
  // adjust distance by random error in direction towards target
  distance = horiz.Magnitude() + forwardError;
  distance = SY_MAX(distance, 1.0f);

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

  SyVect3 launchLoc(pMyObj->GetLocation()), chestLoc(targetLoc);

  cGraphicCharacter* pMyGraphic = static_cast<cGraphicCharacter*>(pMyObj->GetGraphic());
  cGraphicCharacter* pTargetGraphic = prop_cast<cGraphicCharacter*>(pTarget->GetGraphic());

  pMyGraphic->GetIdentNodeLocation(pMyGraphic->GetAnimController()->GetContactNode(), &launchLoc);

  if (pTargetGraphic)
  {
    pTargetGraphic->GetIdentNodeLocation(CHAR_NODE_CHEST, &chestLoc);
  }

  if (SY_FABS(launchLoc.Y-chestLoc.Y) > 0.75f)
  {
    SyVect3 delta;
    delta.Sub(chestLoc,launchLoc);
    pitch = SY_ATAN2( delta.Y, SY_SQRT(delta.X * delta.X + delta.Z * delta.Z));
  }
  else
  {
    pitch = 0.0f;
  }

  aimLoc = pMyObj->GetLocation();
  aimLoc.X += SY_SIN(heading) * distance;
  aimLoc.Y = targetLoc.Y + 0.5f;
  aimLoc.Z += SY_COS(heading) * distance;
}


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

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cGameObject* pTarget = GetTarget();

  float dist = pTarget->GetDistance(GetGameObject());
  float speed = pStats->GetAIMovementSpeed();
  float closeDist = pStats->GetAICloseDistance();

  bool bIsBlocking = CheckBlockOpportunity(time, dist <= closeDist);
  bool bCanAttack = CheckAttackDelay(time);

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

  GAME_ASSERT(ERROR_CODE, prop_cast<cStatsCharacter*>(pTarget->GetStats())!=NULL, "basic attack behavior ai needs character for target");

  if (bIsBlocking)
  {
    return;
  }

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

  if (dist < pStats->GetAIMeleeRange())
  {
    if (mpOwner->IsMeleeAttacker() && !mbInsideMeleeRange)
    {
      cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_ATTACKING_RANGED, GetGameObject()->GetID());
      mbInsideMeleeRange = true;
      mMoveTimer = 0.0f;
      mbUseMoveTimer = false;
      RangedExit();
      MeleeEnter();
      EquipItem(EQUIP_MELEE);
    }
  }
  else
  {
    if (mpOwner->IsRangedAttacker() && mbInsideMeleeRange)
    {
      cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_ATTACKING_MELEE, mpOwner->GetOwner()->GetID());
      mbInsideMeleeRange = false;
      mMoveTimer = 0.0f;
      mbUseMoveTimer = false;
      MeleeExit();
      RangedEnter();
      EquipItem(EQUIP_RANGED);
    }
  }

  // calculate avg velocity
  static const float AVERAGE_VELOCITY_PERIOD = 1.5f;
  SyVect3 frameVel(targetLoc-mLastFrameTargetLoc);
  if (time <= AVERAGE_VELOCITY_PERIOD)
  {
    mTargetAvgVelocity *= AVERAGE_VELOCITY_PERIOD-time;
    mTargetAvgVelocity += targetLoc-mLastFrameTargetLoc;
    mTargetAvgVelocity *= (1.0f/AVERAGE_VELOCITY_PERIOD);
  }
  else
  {
    mTargetAvgVelocity(0.0f, 0.0f, 0.0f);
  }

  RefreshTargeting();

  if (FindTarget(mbInsideMeleeRange))
  {
    if (!bCanAttack)
    {
      DEBUGOVERLAY_DRAWSPHERE(GetGameObject()->GetID(), "AI", myLoc+SyVect3(0.0f, 2.5f, 0.0f), 0.2f, cDebugOverlay::YELLOW);

      UpdateDelayed(time);
    }
    else if (mbInsideMeleeRange)
    {
      UpdateMelee(time);
    }
    else
    {
      UpdateRanged(time);
    }
  }

  mLastFrameTargetLoc = targetLoc;
}

void 
cGoal_Attack::UpdateMelee(float time)
{
  cGameObject* pObj = GetGameObject();
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());
  cGameObject* pTarget = GetTarget();

  float dist = pTarget->GetDistance(GetGameObject());
  float speed = pStats->GetAIMovementSpeed();

  SyVect3 targetLoc(pTarget->GetLocation());
  SyVect3 myLoc(pObj->GetLocation());

  if (mpOwner->TestMaxClosestAttackers(cAIBlackboard::BBR_ATTACKING_MELEE, smMaxMeleeAttackers, mTargetID))
  {
    DEBUGOVERLAY_DRAWSPHERE(pObj->GetID(), "AI", myLoc+SyVect3(0.0f, 2.5f, 0.0f), 0.2f, cDebugOverlay::GREEN);
    if (MoveToMeleePosition(speed))
    {
      mMoveTimer = 0.0f;
      mbUseMoveTimer = false;
      AttackMelee();
      cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_ATTACKING_MELEE,
                                      pObj->GetID(), mTargetID, 0, myLoc);
    }
  }
  else
  {
    DEBUGOVERLAY_DRAWSPHERE(pObj->GetID(), "AI", myLoc+SyVect3(0.0f, 2.5f, 0.0f), 0.2f, cDebugOverlay::RED);
    // if we're waiting for our chance to attack, take a couple steps forward every few seconds
    if (mpOwner->IsOnCamera() && dist >= 2.5f && mbUseMoveTimer)
    {
      MoveToMeleePosition(0.15f);
    }
    else
    {
      GetIntel()->Stop();
      GetIntel()->TurnTo(targetLoc);
    }

    mMoveTimer -= time;

    if (mMoveTimer <= 0.0f)
    {
      static const float BASE_WAIT_TIME = 1.5f;
      static const float MULT_WAIT_TIME = 2.0f;
      mMoveTimer = BASE_WAIT_TIME + (pTarget->GetTitan()->RandomFloat()*MULT_WAIT_TIME);

      mbUseMoveTimer = !mbUseMoveTimer;
    }
  }
}

void 
cGoal_Attack::UpdateRanged(float time)
{
  cGameObject* pObj = GetGameObject();
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());
  float speed = pStats->GetAIMovementSpeed();

  // update flee timer for periodic attack
  static const float MAX_RANGED_FLEE_TIME = 4.0f;
  mMoveTimer = mbUseMoveTimer ? mMoveTimer+time : 0.0f;

  bool bInPositionToAttack = MoveToRangedPosition(speed);  

  if (bInPositionToAttack || mMoveTimer > MAX_RANGED_FLEE_TIME)
  {
    DEBUGOVERLAY_DRAWSPHERE(pObj->GetID(), "AI", pObj->GetLocation()+SyVect3(0.0f, 2.5f, 0.0f), 0.2f, cDebugOverlay::GREEN);

    cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_ATTACKING_RANGED,
                                    pObj->GetID(),
                                    mTargetID, 0, pObj->GetLocation());

    SyVect3 aimLoc;
    float heading, pitch, distance;

    cGameObject* pProp = NULL;
    if (ID_NONE != mTargetExplodingPropID)
    {
      pProp = pObj->GetTitan()->GetRegistry()->Fetch(mTargetExplodingPropID);
      if (!pProp || pProp->GetStats()->IsDead())
      {
        mTargetExplodingPropID = ID_NONE;
      }
    }

    CalculateRangedTargeting(aimLoc, heading, pitch, distance, true);
    GetIntel()->TurnTo(aimLoc);
    AttackRanged(heading, pitch, distance);
    mMoveTimer = 0.0f;

    if (ID_NONE != mTargetExplodingPropID)
    {
      GetAnimInterface()->GetInput()->mTarget = mTargetExplodingPropID;
    }
  }
  else
  {
    DEBUGOVERLAY_DRAWSPHERE(pObj->GetID(), "AI", pObj->GetLocation()+SyVect3(0.0f, 2.5f, 0.0f), 0.2f, cDebugOverlay::RED);
  }
}

void cGoal_Attack::UpdateDelayed(float time)
{
  cGameObject* pTarget = GetTarget();
  cGameObject* pOwner = GetGameObject();
  cIntelNPC* pIntel = GetIntel();
  float dist = pOwner->GetDistance(pTarget);

  if (!mbInsideMeleeRange)
  {
    pIntel->Stop();
    pIntel->TurnTo(pTarget->GetLocation());
  }
  else
  {
    mMoveTimer -= time;

    if (mMoveTimer < 0.0f)
    {
      if (pIntel->IsMoving())
      {
        const float BASE_MOVE_TIME = 0.5f;
        const float MULT_MOVE_TIME = 0.5f;

        mMoveTimer = BASE_MOVE_TIME + pOwner->GetTitan()->RandomFloat()*MULT_MOVE_TIME;

        pIntel->Stop();
        pIntel->TurnTo(pTarget->GetLocation());
      }
      else
      {
        const float BASE_MOVE_TIME = 1.5f;
        const float MULT_MOVE_TIME = 1.0f;

        mMoveTimer = BASE_MOVE_TIME + pOwner->GetTitan()->RandomFloat()*MULT_MOVE_TIME;

        float randMove = pOwner->GetTitan()->RandomFloat();
        SyVect3 moveDir, targetDir(pTarget->GetLocation()-pOwner->GetLocation());

        targetDir.Normalize();

        if (randMove < 0.2f)
        {
          if (dist < 3.0f)
          {
            moveDir = -targetDir;
          }
          else
          {
            moveDir = targetDir;
          }
        }
        else if (randMove < 0.6f)
        {
          moveDir.Cross(targetDir, SyVect3(0.0f, 1.0f, 0.0f));
          moveDir.Normalize();
        }
        else
        {
          moveDir.Cross(SyVect3(0.0f, 1.0f, 0.0f), targetDir);
          moveDir.Normalize();
        }

        moveDir *= 5.0f;
        pIntel->GoTo(pOwner->GetLocation()+moveDir, 1.0f, true, pOwner->GetHeadingTowards(pTarget));
      }
    }
    else
    {
      cAnimCharControllerInput* pInput = GetAnimInterface()->GetInput();

      if (pInput->mbKeepFacing)
      {
        pInput->mFaceHeading = pOwner->GetHeadingTowards(pTarget);
      }
      else
      {
        pIntel->TurnTo(pTarget->GetLocation());
      }
    }
  }
}


bool cGoal_Attack::MoveToMeleePosition(float speed)
{
  GAME_ASSERT(ERROR_CODE, GetTarget() != NULL, "cGoal_Attack::MoveToMeleePosition needs a target");

  if (!GetIntel()->IsAvoiding())
  {
    GetIntel()->GoTo(GetTarget()->GetLocation(), speed);
  }

  return true;
}

bool cGoal_Attack::MoveToRangedPosition(float speed)
{
  GAME_ASSERT(ERROR_CODE, GetTarget() != NULL, "cGoal_Attack::MoveToMeleePosition needs a target");

  cGameObject* pOwner = GetGameObject();
  cGameObject* pTarget = GetTarget();

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pOwner->GetStats());

  SyVect3 targetLoc(pTarget->GetLocation());
  SyVect3 lastSeenTargetLoc;

  float distToTarget = targetLoc.Distance(pOwner->GetLocation());
  bool bInPositionToAttack = true;

  if (distToTarget < pStats->GetAIMeleeRange())
  {
    // we're still in ranged attack mode, but within melee range, 
    // means we have no melee attack, and we must flee
    SyAssert(!mpOwner->IsMeleeAttacker());
    bInPositionToAttack = false;
    mbUseMoveTimer = true;
    if (!GetIntel()->IsAvoiding())
    {
      SyVect3 fleeDir;
      mpOwner->CalculateFleeDirection(pTarget, fleeDir);
      fleeDir *= pStats->GetAIMeleeRange()+1.0f;
      GetIntel()->GoTo(GetGameObject()->GetLocation()+fleeDir, speed); // runaway
//      GetIntel()->GoTo(pOwner->GetLocation()+fleeDir, 1.0f, true, pOwner->GetHeadingTowards(pTarget)); // backpedal
    }
  }
  else
  {
    mbUseMoveTimer = false;

    if ((!mpOwner->CanSeeAttackTarget() && mpOwner->GetLastSeenAttackTargetPos(lastSeenTargetLoc)) ||
      (mpOwner->CanSeeAttackTarget() && distToTarget > smMaxRangedFiringDistance))
    {
      if (!GetIntel()->IsAvoiding())
      {
        GetIntel()->GoTo(lastSeenTargetLoc, speed);
      }

      bInPositionToAttack = false;
    }
    else
    {
      SyVect3 avoidPos;

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

  return bInPositionToAttack;
}

void cGoal_Attack::RegisterTuningVariables()
{
  gTuningSys.AddInt(&smMaxMeleeAttackers, "AI_MeleeCombat_MaxAttackers");
  gTuningSys.AddFloat(&smMaxRangedFiringDistance, "AI_RangedCombat_MaxFiringDistance");
}

//-------------------------------------------------------- cGoal_AttackSwarm

bool cGoal_AttackSwarm::MoveToMeleePosition(float speed)
{
  GAME_ASSERT(ERROR_CODE, GetTarget() != NULL, "cGoal_AttackSwarm::MoveToMeleePosition 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 = 1;
  avoid += myLoc;

  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 && pAttacker != GetGameObject())
      {
        avoid += pAttacker->GetLocation();
        ++numAttackers;
      }
    }
    while (cAIBlackboard::Get()->GetNextRecord(rec));
  }

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

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

  avoid = right;
  avoid *= 2.5f;

  if (!GetIntel()->IsAvoiding())
  {
    GetIntel()->GoTo(GetTarget()->GetLocation()+avoid, speed);
  }

  return true;
}

//-------------------------------------------------------- cGoal_AttackFlank
cGoal_AttackFlank::cGoal_AttackFlank(tGameObjectID target)
: cGoal_Attack(target),
  mFlankPartnerID(ID_NONE),
  mbRequestFlanking(false)
{
}

void cGoal_AttackFlank::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_AttackFlank::MoveToMeleePosition(float speed)
{
  SyAssert(GetTarget() != NULL);

  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;

      if (!GetIntel()->IsAvoiding())
      {
        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 && 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);
      }
    }

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

  return true;
}

//-------------------------------------------------------- cGoal_AttackChargeRetreat
cGoal_AttackChargeRetreat::cGoal_AttackChargeRetreat(tGameObjectID target)
: cGoal_Attack(target),
  mbRetreating(false),
  mRetreatTimer(0.0f)
{
}

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

  static const float RETREAT_TIME = 7.0f;

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cGameObject* pTarget = GetTarget();

  float dist = pTarget->GetDistance(GetGameObject());
  float speed = pStats->GetAIMovementSpeed();

  bool bIsBlocking = CheckBlockOpportunity(time, dist <= pStats->GetAICloseDistance());
  bool bCanAttack = CheckAttackDelay(time);

  if (bIsBlocking)
  {
    return;
  }

  if (!mbRetreating && GetAnimInterface()->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))    
  {
    if (bCanAttack)
    {
      MoveToMeleePosition(speed);
      AttackMelee();
    }
    else if (dist > 2.0f)
    {
      MoveToMeleePosition(speed);
    }
  }
}

//-------------------------------------------------------- cGoal_AttackWander
cGoal_AttackWander::cGoal_AttackWander(tGameObjectID target)
: cGoal_Attack(target),
  mAttackTimer(0.0f)
{
}

void cGoal_AttackWander::AttackEnter()
{
  mAttackTimer = 0.0f;
  mWander.Init(GetGameObject(), 0.8f, 0.1f);
}


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

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cGameObject* pTarget = GetTarget();

  float distToTarget = pTarget->GetDistance(GetGameObject());
  float speed = pStats->GetAIMovementSpeed();
  float range = GetAnimInterface()->GetRange(COMBO_L);

  bool bIsBlocking = CheckBlockOpportunity(time, distToTarget <= pStats->GetAICloseDistance());
  bool bCanAttack = CheckAttackDelay(time);

  if (bIsBlocking)
  {
    return;
  }

  static const float MELEEWANDER_MAX_ATTACK_TIME = 2.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))
    {
      // don't test the limit of max attackers - it's your own 
      // dumbass fault for standing next to them!
      GetIntel()->TurnTo(pTarget->GetLocation());

      if (bCanAttack)
      {
        AttackMelee();
      }
    }
  }
  else 
  {
    if (distToTarget > range)
    {
      mAttackTimer = 0.0f;
    }

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

//-------------------------------------------------------- cGoal_AttackSentinel
void cGoal_AttackSentinel::UpdateDelayed(float time)
{
  GetIntel()->Stop();

  cGameObject* pTarget = GetTarget();
  if (pTarget && mpOwner->CanSeeAttackTarget())
  {
    GetIntel()->TurnTo(pTarget->GetLocation());
  }
}

bool cGoal_AttackSentinel::MoveToMeleePosition(float speed)
{
  cGameObject* pTarget = GetTarget();
  if (pTarget)
  {
    GetIntel()->TurnTo(pTarget->GetLocation());
  }

  return true;
}

bool cGoal_AttackSentinel::MoveToRangedPosition(float speed)
{
  return true;
}

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

void cGoal_Kamikaze::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());
}

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

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cGameObject* pTarget = GetTarget();
  float speed = pStats->GetAIMovementSpeed();

  if (FindTarget(true))
  {
    MoveToMeleePosition(speed);
  }

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

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

//-------------------------------------------------------- cGoal_AttackStealth
cGoal_AttackStealth::cGoal_AttackStealth(tGameObjectID target)
: cGoal_Attack(target),
  mbStartedAttack(false),
  mbInvisible(false),
  mbWasAttacking(false),
  mInvisibleTimer(0.0f)
{
}


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

void cGoal_AttackStealth::AttackExit()
{
  SetInvisible(false);
}

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

    if (mbInvisible)
    {
      GetGameObject()->GetStats()->AddCondition("Invisible", 0, 0, 0, -1.0f, ID_NONE);
    }
    else
    {
      GetGameObject()->GetStats()->RemoveCondition("Invisible", 0, 0, 0);
    }
  }
}

void cGoal_AttackStealth::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_Attack::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_AttackStealth::MoveToMeleePosition(float speed)
{
  cGameObject* pTarget = GetTarget();
  SyAssert(pTarget != NULL);

  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;
  }

  if (!GetIntel()->IsAvoiding())
  {
    GetIntel()->GoTo(targetLoc+adjust, speed);
  }

  return true;
}

//-------------------------------------------------------- cGoal_AttackStun
cGoal_AttackStun::cGoal_AttackStun(tGameObjectID target)
: cGoal_Attack(target),
  mbCanIStun(false)
{
}


void cGoal_AttackStun::AttackEnter()
{ 
  mbCanIStun = false;

  cGameObject* pObj = GetGameObject();
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());
  const cStatsAbilitySet* pAbilitySet = pStats->GetAbilitySet();

  if (pAbilitySet)
  { 
    for (int i=pAbilitySet->mEntries.Begin(); i!=pAbilitySet->mEntries.End(); i=pAbilitySet->mEntries.Next(i))
    {
      if (cStatsAbilitySet::ABILITY_CONDITION_MELEE_ATTACK == pAbilitySet->mEntries(i).mCondition ||
          cStatsAbilitySet::ABILITY_CONDITION_RANGED_ATTACK == pAbilitySet->mEntries(i).mCondition)
      {
        if (ID_NONE != pAbilitySet->mEntries(i).mAbilityID1 && 
            DoesAbilityStun(pAbilitySet->mEntries(i).mAbilityID1))
        {
          mbCanIStun = true;
          break;
        }

        if (ID_NONE != pAbilitySet->mEntries(i).mAbilityID2 && 
            DoesAbilityStun(pAbilitySet->mEntries(i).mAbilityID2))
        {
          mbCanIStun = true;
          break;
        }

        if (ID_NONE != pAbilitySet->mEntries(i).mAbilityID3 && 
            DoesAbilityStun(pAbilitySet->mEntries(i).mAbilityID3))
        {
          mbCanIStun = true;
          break;
        }
      }
    }
  }
}

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

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

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

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

  if (bTargetStunned)
  {
    cGoal_Attack::Update(time);

    if (mbCanIStun)
    {
      cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_STUNNING, GetGameObject()->GetID());
    }
  }
  else
  {
    if (mbCanIStun)
    {
      cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_STUNNING, GetGameObject()->GetID(), pTarget->GetID());
      cGoal_Attack::Update(time);
    }
    else
    {
      cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
      float speed = pStats->GetAIMovementSpeed();
      float distToTarget = GetGameObject()->GetDistance(pTarget);

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

bool cGoal_AttackStun::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 && pAttacker != GetGameObject())
      {
        bFoundAnotherStunner = true;
      }
    }
    while (!bFoundAnotherStunner && cAIBlackboard::Get()->GetNextRecord(rec));
  }

  return bFoundAnotherStunner;
}

bool cGoal_AttackStun::DoesAbilityStun(tGameID abilityID)
{
  cGameObject* pObj = GetGameObject();
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());
  const cAbilityMaster* pAbility = pStats->GetAbility(abilityID);
  const cSpellMaster* pSpell;

  if (pAbility)
  {
    tGameID spellID = ID_NONE;
    if (NUM_COMBOS != pAbility->mCombo)
    {
      spellID = static_cast<cGraphicCharacter*>(pObj->GetGraphic())->GetAnimController()->GetComboSpellID(pAbility->mCombo);
    }
    else
    {
      spellID = pAbility->mSpell;
    }

    if (ID_NONE != spellID)
    {
      pSpell = pObj->GetTitan()->GetDatabaseSys()->GetSpellMaster(spellID);

      if (pSpell)
      {
        for (int i=pSpell->mEffects.Begin(); i!=pSpell->mEffects.End(); i=pSpell->mEffects.Next(i))
        {
          if (cSpellEffectMaster::SPELLEFFECT_STUN == pSpell->mEffects(i)->mType)
          {
            return true;
          }
        }
      }
    }
  }

  return false;
}


//----------------------------------------------------------cGoal_AttackCircle
cGoal_AttackCircle::cGoal_AttackCircle(tGameObjectID target)
: cGoal_Attack(target),
  mState(MELEE_STATE_NONE),
  mCircleToAttackTimer(0.0f),
  mCircleRepeatTimer(0.0f),
  mRetreatDir(0.0f, 0.0f, 0.0f)
{
}

void cGoal_AttackCircle::MeleeEnter()
{
  SyAssert(GetTarget()!=NULL);

  cGameObject* pObj = GetGameObject();

  mState = MELEE_STATE_NONE;

  mCircleToAttackTimer = 0.0f;

  static const float INITIAL_CIRCLE_BASE_TIME = 0.5f;
  static const float INITIAL_CIRCLE_MULT_TIME = 1.5f;
  mCircleRepeatTimer = INITIAL_CIRCLE_BASE_TIME + (pObj->GetTitan()->RandomFloat()*INITIAL_CIRCLE_MULT_TIME);

  cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_MELEE_CIRCLE, pObj->GetID(), GetTarget()->GetID(), mState);
}

void cGoal_AttackCircle::MeleeExit()
{
  cAIBlackboard::Get()->RemoveRecords(cAIBlackboard::BBR_MELEE_CIRCLE, GetGameObject()->GetID());
}

bool cGoal_AttackCircle::FindNeighboringAttackerHeadings(float& lesserHeading, float& greaterHeading)
{
  // find headings of attackers on either side of this one
  cGameObject* pObj = GetGameObject();
  cGameObject* pTarget = GetTarget();
  cGameObject* pAttacker;
  cAIBlackboard::Record rec;
  float myHeading = pTarget->GetHeadingTowards(pObj);
  bool bFoundNeighbors = false;

  std::vector<float> headings;

  if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_MELEE_CIRCLE, ID_NONE, mTargetID, rec))
  { 
    do
    {
      pAttacker = pObj->GetTitan()->GetRegistry()->Fetch(rec.mSourceID);        
      if (pAttacker)
      {
        headings.push_back(pTarget->GetHeadingTowards(pAttacker));
      }
    }
    while (cAIBlackboard::Get()->GetNextRecord(rec));
  }

  if (headings.size() <= 2)
  {
    lesserHeading = greaterHeading = myHeading;
    return false;
  }
  else if (headings.size() > 2)
  {
    std::sort(headings.begin(), headings.end());

    for (unsigned int i=0; i < headings.size()-1; ++i)
    {
      if (SY_FABS(myHeading-headings[i]) < 0.00001f)
      {
        if (0 == i)
        {
          lesserHeading = headings[headings.size()-1];
        }
        else
        {
          lesserHeading = headings[i-1];
        }

        if (i == headings.size()-1)
        {
          greaterHeading = headings[0];
        }
        else
        {
          greaterHeading = headings[i+1];
        }

        bFoundNeighbors = true;
        break;
      }
    }
  }

  while (lesserHeading < 0.0f)
  {
    lesserHeading += SY_PI*2.0f;
  }

  while (lesserHeading > SY_PI*2.0f)
  {
    lesserHeading -= SY_PI*2.0f;
  }

  while (greaterHeading < 0.0f)
  {
    greaterHeading += SY_PI*2.0f;
  }

  while (greaterHeading > SY_PI*2.0f)
  {
    greaterHeading -= SY_PI*2.0f;
  }

  return bFoundNeighbors;
}

void cGoal_AttackCircle::UpdateDelayed(float time)
{
  cGameObject* pObj = GetGameObject();

  if (mbInsideMeleeRange)
  {
    cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());

    if (MELEE_STATE_ATTACK == mState)
    {
      mState = MELEE_STATE_RETREAT;

      // calculate direction to run away to
      float retreatHeading = GetTarget()->GetHeadingTowards(pObj);
      float lesserHeading, greaterHeading;

      if (FindNeighboringAttackerHeadings(lesserHeading, greaterHeading))
      {
        retreatHeading = (lesserHeading + greaterHeading)*0.5f;

        if (lesserHeading > greaterHeading)
        {
          retreatHeading += SY_PI;
        }
      }

      mRetreatDir.HPR(retreatHeading, 0.0f, 0.0f);
      mRetreatDir.Normalize();
      float goalDist = pStats->GetAIMeleeRange()*(0.4f + (pObj->GetTitan()->RandomFloat()*0.6f));
      mRetreatDir *= goalDist;
    }

    float speed = pStats->GetAIMovementSpeed();

    MoveToMeleePosition(speed);

    cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_MELEE_CIRCLE, GetGameObject()->GetID(), GetTarget()->GetID(), mState);
  }
  else
  {
    cGoal_Attack::UpdateDelayed(time);
  }
}

void cGoal_AttackCircle::UpdateMelee(float time)
{
  cGameObject* pObj = GetGameObject();
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());
  cGameObject* pTarget = GetTarget();

  float speed = pStats->GetAIMovementSpeed();

  SyVect3 targetLoc(pTarget->GetLocation());
  SyVect3 myLoc(pObj->GetLocation());

  mCircleRepeatTimer -= time;

  if (MoveToMeleePosition(speed))
  {
    if (MELEE_STATE_ATTACK == mState)
    {
      GetIntel()->TurnTo(targetLoc);
      AttackMelee();
      cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_ATTACKING_MELEE,
                                      pObj->GetID(), mTargetID, 0, myLoc);
    }
    else
    {
      if (MELEE_STATE_CIRCLE_RIGHT == mState || MELEE_STATE_CIRCLE_LEFT == mState)
      {
        mCircleToAttackTimer -= time;
      }

      if (mCircleToAttackTimer <= 0.0f)
      {
        cAIBlackboard::Record rec;
        cGameObject* pAttacker;
        bool bFoundOtherAttacker = false;
        uint64 curTime = pObj->GetTitan()->GetTime();
        
        static uint64 ATTACKER_TIMEOUT = 10000;

        if (cAIBlackboard::Get()->GetFirstRecord(cAIBlackboard::BBR_MELEE_CIRCLE, ID_NONE, mTargetID, rec))
        { 
          do
          {
            pAttacker = GetRegistry()->Fetch(rec.mSourceID);        
            if (pAttacker &&
                pAttacker != pObj &&
                !pAttacker->GetStats()->IsDead() &&
                rec.mData == MELEE_STATE_ATTACK && 
                (curTime - rec.mCreatedTimestamp < ATTACKER_TIMEOUT))
            {
              bFoundOtherAttacker = true;
              break;
            }
          }
          while (cAIBlackboard::Get()->GetNextRecord(rec));

          if (!bFoundOtherAttacker)
          {
            mState = MELEE_STATE_ATTACK;
          }
        }
      }

      GetIntel()->TurnTo(targetLoc);
    }
  }

  cAIBlackboard::Get()->AddRecord(cAIBlackboard::BBR_MELEE_CIRCLE, GetGameObject()->GetID(), GetTarget()->GetID(), mState);
}



bool cGoal_AttackCircle::MoveToMeleePosition(float speed)
{
  cGameObject* pObj = GetGameObject();
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());
  cGameObject* pTarget = GetTarget();
  SyVect3 myLoc(pObj->GetLocation());
  SyVect3 targetLoc(pTarget->GetLocation());
  SyVect3 targetDir(myLoc-targetLoc);
  float distToTarget = targetDir.NormalizeMagn();
  
  cAnimCharControllerInput* pInput = GetAnimInterface()->GetInput();
  pInput->mDodging = false;

  if (MELEE_STATE_ATTACK == mState)
  {
    return cGoal_Attack::MoveToMeleePosition(speed);
  }
  else if (MELEE_STATE_RETREAT == mState)
  {
    SyVect3 retreatLoc(targetLoc);

    if (mRetreatDir.MagnitudeSquared() < 0.00001f)
    {
      mRetreatDir = targetDir;
      float goalDist = pStats->GetAIMeleeRange()*(0.4f + (pObj->GetTitan()->RandomFloat()*0.6f));
      mRetreatDir *= goalDist;
    }

    retreatLoc += mRetreatDir;

    if (pObj->GetDistance(retreatLoc) >= 0.1f)
    {
      if (!GetIntel()->IsAvoiding())
      {
        GetIntel()->GoTo(retreatLoc, pStats->GetAIMovementSpeed());
        return false;
      }
      else
      {
        // if we're avoiding we should stop retreating
        mState = MELEE_STATE_NONE;
      }
    }
  }

  if (mCircleRepeatTimer < 0.000001f)
  {
    bool bLeft = true;
    if (MELEE_STATE_CIRCLE_RIGHT == mState)
    {
      bLeft = false;
    }
    else if (MELEE_STATE_CIRCLE_LEFT == mState)
    {
      bLeft = true;
    }
    else
    {
      bLeft = pObj->GetTitan()->Random(0, 1) == 0;

      static const float BASE_ATTACK_DELAY = 2.0f;
      static const float MULT_ATTACK_DELAY = 1.0f;
      mCircleToAttackTimer = BASE_ATTACK_DELAY + pObj->GetTitan()->RandomFloat()*MULT_ATTACK_DELAY;

      if (bLeft)
      {
        mState = MELEE_STATE_CIRCLE_LEFT;
      }
      else
      {
        mState = MELEE_STATE_CIRCLE_RIGHT;
      }
    }

    SyVect3 rightTargetDir;
    float heading, pitch, roll;
    rightTargetDir.Cross(targetDir, SyVect3(0.0f, 1.0f, 0.f));
    rightTargetDir.Normalize();    

    if (bLeft)
    {
      rightTargetDir = -rightTargetDir;
    }

    rightTargetDir.MulAdd(targetDir, mRetreatDir.Magnitude()-distToTarget);
    rightTargetDir.HPR(&heading, &pitch, &roll);
    
    pInput->mDodging = true;
    pInput->mDodgeDirection = heading;   

    GetIntel()->Stop();

    static const float CIRCLE_REPEAT_BASE_TIME = 1.0f;
    static const float CIRCLE_REPEAT_MULT_TIME = 2.5f;
    mCircleRepeatTimer = CIRCLE_REPEAT_BASE_TIME + (pTarget->GetTitan()->RandomFloat()*CIRCLE_REPEAT_MULT_TIME);
  }
  else
  {
    GetIntel()->Stop();
    GetIntel()->TurnTo(targetLoc);
  }

  return true;
}

bool cGoal_AttackCircle::MoveToRangedPosition(float speed)
{
  static const float SEPARATED_RADIUS = 10.0f;
  static const float SEPARATED_TOLERANCE = 4.0f;
  bool bInAttackPosition = cGoal_Attack::MoveToRangedPosition(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 && 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;
//      DEBUGOVERLAY_DRAWLINE(GetGameObject()->GetID(), "AI", myLoc+SyVect3(0.0f, 2.0f, 0.0f), myLoc+leftAdjust+SyVect3(0.0f, 2.0f, 0.0f), cDebugOverlay::BLUE);
    }

    if (pRightClosestAttacker && rightClosestDist < maxAttackerAdjust)
    {
      rightAdjust = -rightTargetDir;
      rightAdjust *= maxAttackerAdjust-rightClosestDist;
//      DEBUGOVERLAY_DRAWLINE(GetGameObject()->GetID(), "AI", myLoc+SyVect3(0.0f, 2.0f, 0.0f), myLoc+rightAdjust+SyVect3(0.0f, 2.0f, 0.0f), cDebugOverlay::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;
//      DEBUGOVERLAY_DRAWLINE(GetGameObject()->GetID(),"AI",  myLoc+SyVect3(0.0f, 2.0f, 0.0f), myLoc+avoid+SyVect3(0.0f, 2.0f, 0.0f), cDebugOverlay::RED);
    }

    avoid += leftAdjust;
    avoid += rightAdjust;

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

  return true;
}

//----------------------------------------------------------cGoal_AttackLine
cGoal_AttackLine::cGoal_AttackLine(tGameObjectID target)
: cGoal_Attack(target)
{
}

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

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

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

bool cGoal_AttackLine::MoveToRangedPosition(float speed)
{
  static const float MINIMUM_FORWARD_DISTANCE = 8.0f;
  static const float MINIMUM_SIDEWAYS_DISTANCE = 4.0f;
  static const float SIDEWAYS_TOLERANCE = 2.0f;
  static const float FORWARD_TOLERANCE = 2.0f;

  bool bInAttackPosition = cGoal_Attack::MoveToRangedPosition(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 && 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), cDebugOverlay::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;
//        DEBUGOVERLAY_DRAWLINE(GetGameObject()->GetID(), "AI", myLoc+SyVect3(0.0f, 3.0f, 0.0f), myLoc+sidewaysAdjust+SyVect3(0.0f, 3.0f, 0.0f), ((pClosestAttacker->GetLocation()-myLoc)^rightTargetDir)> 0.0f?cDebugOverlay::MAGENTA:cDebugOverlay::YELLOW);
      }
    }

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

  return true;
}


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

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cGameObject* pTarget = GetTarget();

  float dist = pTarget->GetDistance(GetGameObject());
  float speed = pStats->GetAIMovementSpeed();
  float closeDist = pStats->GetAICloseDistance();

  bool bIsBlocking = CheckBlockOpportunity(time, dist <= closeDist);
  bool bCanAttack = CheckAttackDelay(time);

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

  if (bIsBlocking)
  {
    return;
  }

  if (FindTarget(false))
  {
    if (!bCanAttack && dist >= 2.0f)
    {
      if (!GetIntel()->IsAvoiding())
      {
        GetIntel()->GoTo(targetLoc, speed);
      }
    }
    else if (bCanAttack)
    {
      AttackMelee();
    }
  }
}
//----------------------------------------------------------cGoal_AttackArea
cGoal_AttackArea::cGoal_AttackArea(tGameObjectID target)
: cGoal_Attack(target),
  mLOSWaitTime(0.0f),
  mbFindNewArea(true)
{
}

void cGoal_AttackArea::UpdateRanged(float time)
{
  if (!mbFindNewArea)
  {
    mTargetAreaLOS.Update(GetGameObject(), time);
    if (mTargetAreaLOS.mbUseTargetPos && !mTargetAreaLOS.CanSeeTarget())
    {
      mLOSWaitTime += time;

      static const float CASTAREAEFFECT_MAX_LOS_WAITTIME = 5.0f;

      if (mLOSWaitTime > CASTAREAEFFECT_MAX_LOS_WAITTIME)
      {
        mbFindNewArea = true;
        mTargetAreaLOS.Clear();
      }
    }
    //    DEBUGOVERLAY_DRAWLINE(GetGameObject()->GetID(), "AI", GetGameObject()->GetLocation()+SyVect3(0.0f, 3.0f, 0.0f), mSpellTargetLOS.mTargetPos+SyVect3(0.0f, 3.0f, 0.0f), cDebugOverlay::WHITE);
  }

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  const cStatsAbilitySet* pAbilitySet = pStats->GetAbilitySet();

  if (pAbilitySet)
  {
    tGameID curAbilityID = mCurAttackAbilityID;
    tGameID nextAbilityID = ID_NONE;
    mCurAttackAbilityID = ID_NONE;

    if (pAbilitySet->GetAbility(cStatsAbilitySet::ABILITY_CONDITION_RANGED_ATTACK, curAbilityID, &nextAbilityID))
    {
      if (nextAbilityID != mCurAttackAbilityID)
      {
        mCurAttackAbilityID = nextAbilityID;
      }
    }

    if (ID_NONE == mCurAttackAbilityID)
    {
      mCurAttackAbilityID = curAbilityID;
    }
  }


  cGoal_Attack::UpdateRanged(time);
}

bool cGoal_AttackArea::MoveToRangedPosition(float speed)
{
  // can see target pos, now test if we're in range
  const cSpellMaster* pSpell = GetSpell();
  
  if (!pSpell)
  {
    return cGoal_Attack::MoveToRangedPosition(speed);
  }

//  SyAssertf(pSpell->mRange > pSpell->mWidth, "Area of effect spell AI has been given spell '%s' with a range inside it's area of effect (range too small)", pSpell->mID.GetName());

  if (mbFindNewArea)
  {
    SyVect3 targetPos;
    if (SelectRadiusPosition(targetPos))
    {
      targetPos.Y += 0.5f;
      mTargetAreaLOS.SetTargetPosition(targetPos);
      mLOSWaitTime = 0.0f;
      mbFindNewArea = false;
    }
    else
    {
      GetIntel()->Stop();
      GetIntel()->TurnTo(mTargetAreaLOS.mTargetPos);
      return false;
    }
  }

  cGameObject* pObj = GetGameObject();
  //cStatsCharacter* pStats = static_cast<cStatsCharacter*>(pObj->GetStats());

  SyVect3 toTarget(mTargetAreaLOS.mTargetPos-pObj->GetLocation());
  float distToTarget = toTarget.NormalizeMagn();

  if (!mTargetAreaLOS.CanSeeTarget() && !mTargetAreaLOS.IsLOSPending())
  {
    SyVect3 blockedPoint;
    if (mTargetAreaLOS.GetCollisionPoint(blockedPoint))
    {
      toTarget = mTargetAreaLOS.mTargetPos-pObj->GetLocation();
      distToTarget = toTarget.NormalizeMagn();
      if (SY_FABS(distToTarget-pSpell->mRange) > pSpell->mWidth)
      {
        if (!GetIntel()->IsAvoiding())
        {
          GetIntel()->GoTo(blockedPoint, speed);
        }
      }
      else
      {
        GetIntel()->Stop();
        GetIntel()->TurnTo(mTargetAreaLOS.mTargetPos);
        mbFindNewArea = true;
      }
    }
    else if (!GetIntel()->IsAvoiding())
    {
      GetIntel()->GoTo(mTargetAreaLOS.mTargetPos, speed);
    }
    else
    {
      GetIntel()->Stop();
      GetIntel()->TurnTo(mTargetAreaLOS.mTargetPos);
      mbFindNewArea = true;
    }

    return false;
  }

  if (distToTarget > pSpell->mRange || distToTarget < pSpell->mWidth)
  {
    toTarget *= distToTarget - (pSpell->mWidth+1.0f);

    if (toTarget.Magnitude() > 1.0f)
    {
      if (!GetIntel()->IsAvoiding())
      {
        GetIntel()->GoTo(pObj->GetLocation()+toTarget, speed);
      }
      else
      {
        GetIntel()->Stop();
        GetIntel()->TurnTo(mTargetAreaLOS.mTargetPos);
        mbFindNewArea = true;
      }

      return false;
    }
  }

  GetIntel()->Stop();
  GetIntel()->TurnTo(mTargetAreaLOS.mTargetPos);
  mbFindNewArea = true;
  return true;
}

struct TestTargetPosition
{
  SyVect3 mCenter;
  int mNumTargets;
};

bool cGoal_AttackArea::SelectRadiusPosition(SyVect3& targetPos)
{
  const cSpellMaster* pSpell = GetSpell();

  if (!pSpell)
  {
    return false;
  }

  float effectRadius = pSpell->mWidth*0.5f;
  float minRadius = effectRadius*0.707f; 
  cGameObjectRegistry* pRegistry = GetRegistry();
  SyAssert(pRegistry != NULL);

  bool bMatchedTargetPos;

  std::vector<TestTargetPosition> targetPositions;

  cGameObject* pObj = pRegistry->Begin();
  cIntelNPC* pMyIntel = GetIntel();

  for(;pObj != NULL;pObj= pRegistry->Next(pObj))
  {
    if (!pObj || !pMyIntel->IsTargetable(pObj))
    {
      continue;
    }

    bMatchedTargetPos = false;

    if (targetPositions.size() > 0)
    {
      for (unsigned int i=0; i<targetPositions.size() && !bMatchedTargetPos; ++i)
      {
        if (targetPositions[i].mCenter.DistanceSquared(pObj->GetLocation()) <= minRadius*minRadius)
        {
          targetPositions[i].mCenter *= (float)(targetPositions[i].mNumTargets);
          targetPositions[i].mCenter += pObj->GetLocation();
          targetPositions[i].mCenter /= (float)(++targetPositions[i].mNumTargets);
          bMatchedTargetPos = true;
          break;
        }
      }
    }

    if (!bMatchedTargetPos)
    {
      TestTargetPosition t;
      t.mCenter = pObj->GetLocation();
      t.mNumTargets = 1;
      targetPositions.push_back(t);
    }
  }

  if (targetPositions.size() > 0)
  {
    targetPos = targetPositions[0].mCenter;
    return true;
  }

  return false;
}

bool cGoal_AttackArea::SelectWallPosition(SyVect3& targetPos)
{
  std::vector<cGameObject*> friends;
  std::vector<cGameObject*> enemies;

  const cSpellMaster* pSpell = GetSpell();

  if (!pSpell)
  {
    return false;
  }

  //  bool bCastLengthWise = pSpell->mRange > pSpell->mWidth;
  //  float forwardLen = bCastLengthWise ? pSpell->mWidth : pSpell->mRange;
  //  float sideLen = bCastLengthWise ? pSpell->mRange : pSpell->mWidth;

  cGameObjectRegistry* pRegistry = GetRegistry();
  SyAssert(pRegistry != NULL);

  cGameObject* pObj;
  cGameObject* pOwner = GetGameObject();
  cIntelNPC* pIntel = GetIntel();

  SyVect3 attackCenter(0.0f, 0.0f, 0.0f);
  for(pObj = pRegistry->Begin(); pObj != NULL; pObj= pRegistry->Next(pObj))
  {
    if (!pObj || pObj == pOwner ||
      (pObj->GetType() != cGameObject::OBJ_NPC && 
      pObj->GetType() != cGameObject::OBJ_PLAYER) ||
      pOwner->GetDistance(pObj) > 40.0f)
    {
      continue;
    }

    if (pIntel->IsTargetable(pObj))
    {
      enemies.push_back(pObj);
    }
    else if (pIntel->IsFriendly(pObj))
    {
      friends.push_back(pObj);
    }
  }

  if (enemies.size() == 0)
  {
    return false;
  }

  if (friends.size() == 0 || friends.size() == 1)
  {
    cGameObject* pClosest = NULL;
    float closestDist = 0.0f, dist;
    std::vector<cGameObject*>::iterator iEnemy = enemies.begin();
    std::vector<cGameObject*>::iterator endEnemy = enemies.end();
    for (; iEnemy != endEnemy; ++iEnemy)
    {
      dist = pOwner->GetDistance((*iEnemy)->GetLocation());
      if (!pClosest || dist < closestDist)
      {
        closestDist = dist;
        pClosest = *iEnemy; 
      }
    }

    SyAssert(pClosest != NULL);
    targetPos = pClosest->GetLocation() + pOwner->GetLocation();
    targetPos *= 0.5f;
    return true;
  }
  else if (friends.size() >= 2)
  {
    if (enemies.size() == 1)
    {
    }
    else if (enemies.size() == 2)
    {
    }
    else if (enemies.size() >= 3)
    {

    }
  }
  return false;
}

const cSpellMaster* cGoal_AttackArea::GetSpell()
{
  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cDatabaseSys* pDB = GetGameObject()->GetTitan()->GetDatabaseSys();
  const cAbilityMaster* pRangedAbility = NULL;

  if (ID_NONE != mCurAttackAbilityID)
  {
    pRangedAbility = pStats->GetAbility(mCurAttackAbilityID);
    GAME_ASSERT(ERROR_CODE, pRangedAbility!=NULL, "Could not find ranged attack ability for NPC");
    GAME_ASSERT(ERROR_CODE, pRangedAbility->mSpell!=ID_NONE, "Area of effect casting AI needs ranged attack ability with a spell!");

    if (pRangedAbility && ID_NONE != pRangedAbility->mSpell)
    {
      return pDB->GetSpellMaster(pRangedAbility->mSpell);
    }
  }

  return NULL;
}


void cGoal_AttackArea::AttackRanged(float heading, float pitch, float distance)
{
  if (ID_NONE == mCurAttackAbilityID)
  {
    SyAssert(false);
    return;
  }

  cStatsCharacter* pStats = static_cast<cStatsCharacter*>(GetGameObject()->GetStats());
  cDatabaseSys* pDB = GetGameObject()->GetTitan()->GetDatabaseSys();
  const cAbilityMaster* pRangedAbility = NULL;

  pRangedAbility = pStats->GetAbility(mCurAttackAbilityID);
  GAME_ASSERT(ERROR_CODE, pRangedAbility!=NULL, "Could not find ranged attack ability for NPC");
  GAME_ASSERT(ERROR_CODE, pRangedAbility->mSpell!=ID_NONE, "Area of effect casting AI needs ranged attack ability with a spell!");

  if (pRangedAbility && ID_NONE != pRangedAbility->mSpell)
  {
    const cSpellMaster* pSpell = pDB->GetSpellMaster(pRangedAbility->mSpell);
    if (pSpell)
    {
      pSpell->CastSpell(GetGameObject(), NULL, &mTargetAreaLOS.mTargetPos);

      cAnimCharControllerInput* pInput = GetAnimInterface()->GetInput();
      pInput->mTarget = ID_NONE;
      pInput->mTargetHeading = heading;
      pInput->mTargetPitch = pitch;
      pInput->mTargetRange = distance; 

      return;
    }
  }

  GetIntel()->AttackRanged(heading, pitch, distance);
}

// EOF
