[Home]

Jailbreak Developer Network: JBBotSquad/Implementation


(Internal) Actor >> Info >> ReplicationInfo >> SquadAI >> JBBotSquad

// ============================================================================
// JBBotSquad
// Copyright 2002 by Mychaeel <mychaeel@planetjailbreak.com>
// $Id: JBBotSquad.uc,v 1.21 2007/04/24 16:26:09 jrubzjeknf Exp $
//
// Controls the bots of an attacking, freelancing or defending squad.
// ============================================================================


class JBBotSquad extends SquadAI
  notplaceable;


// ============================================================================
// Types
// ============================================================================

struct TInfoEnemy
{
  var float TimeUpdate;         // time of last update
  var bool bIsApproaching;      // enemy was approaching objective when visible
  var bool bIsVisible;          // enemy was visible at last update
  var float DistanceObjective;  // distance of enemy to defended objective
};


struct TInfoHunt
{
  var Controller Controller;            // hunted player
  var NavigationPoint NavigationPoint;  // last known location
};


// ============================================================================
// Variables
// ============================================================================

var private float TimeInitialized;        // time of squad initialization

var private TInfoEnemy ListInfoEnemy[8];  // indexed like Enemies array
var private TInfoHunt InfoHunt;           // information about player hunt


// ============================================================================
// Caches
// ============================================================================

struct TCacheCountEnemies { var float Time; var int Result; };

var private transient TCacheCountEnemies CacheCountEnemies;


// ============================================================================
// PostBeginPlay
//
// Records the time this squad was created at.
// ============================================================================

event PostBeginPlay()
{
  TimeInitialized = Level.TimeSeconds;
}


// ============================================================================
// Initialize
//
// Initializes this squad with the given team, objective and leader. Unlike
// its superclass counterpart, it forces bots to check their new assignments
// to prevent them from being stuck in camping mode in jail.
// ============================================================================

function Initialize(UnrealTeamInfo UnrealTeamInfo, GameObjective GameObjective, Controller ControllerLeader)
{
  Team = UnrealTeamInfo;

  SetLeader(ControllerLeader);
  SetObjective(GameObjective, True);  // force reassessment
}


// ============================================================================
// FindPathToObjective
//
// If the given objective is a JBGameObjective, finds the path to the
// associated trigger instead.
// ============================================================================

function bool FindPathToObjective(Bot Bot, Actor ActorObjective)
{
  if (JBGameObjective(ActorObjective) != None)
    ActorObjective = JBGameObjective(ActorObjective).TriggerRelease;

  return Super.FindPathToObjective(Bot, ActorObjective);
}


// ============================================================================
// SetEnemy
//
// Only acquires free players as new enemies.
// ============================================================================

function bool SetEnemy(Bot Bot, Pawn PawnEnemy)
{
  local JBGameRules firstJBGameRules;
  local JBTagPlayer TagPlayerEnemy;

  firstJBGameRules = Jailbreak(Level.Game).GetFirstJBGameRules();
  if (firstJBGameRules != None &&
     !firstJBGameRules.CanBotAttackEnemy(Bot, PawnEnemy))
    return False;

  if (PawnEnemy != None)
    TagPlayerEnemy = Class'JBTagPlayer'.Static.FindFor(PawnEnemy.PlayerReplicationInfo);

  if (TagPlayerEnemy == None ||
      TagPlayerEnemy.IsFree())
    return Super.SetEnemy(Bot, PawnEnemy);

  return False;
}


// ============================================================================
// IsEnemyAcquired
//
// Checks and returns whether the given enemy player has been acquired by this
// squad.
// ============================================================================

function bool IsEnemyAcquired(Controller Controller)
{
  local int iEnemy;

  for (iEnemy = 0; iEnemy < ArrayCount(Enemies); iEnemy++)
    if (Enemies[iEnemy] != None &&
        Enemies[iEnemy].Controller == Controller)
      return True;

  return False;
}


// ============================================================================
// CountEnemies
//
// Returns the number of enemies this squad is currently engaged in fight with.
// Result is cached within a tick.
// ============================================================================

function int CountEnemies()
{
  local int iEnemy;

  if (CacheCountEnemies.Time == Level.TimeSeconds)
    return CacheCountEnemies.Result;

  CacheCountEnemies.Result = 0;
  for (iEnemy = 0; iEnemy < ArrayCount(Enemies); iEnemy++)
    if (Enemies[iEnemy] != None)
      CacheCountEnemies.Result += 1;

  CacheCountEnemies.Time = Level.TimeSeconds;
  return CacheCountEnemies.Result;
}


// ============================================================================
// ClearEnemies
//
// Clears the list of enemies acquired by this squad.
// ============================================================================

function ClearEnemies()
{
  local int iEnemy;
  local Bot thisBot;

  TimeInitialized = Level.TimeSeconds;

  for (iEnemy = 0; iEnemy < ArrayCount(Enemies); iEnemy++)
    Enemies[iEnemy] = None;

  for (thisBot = SquadMembers; thisBot != None; thisBot = thisBot.NextSquadMember)
    thisBot.Enemy = None;

  ClearHunt();
}


// ============================================================================
// GetSize
//
// Unlike its superclass counterpart, returns the actual number of human
// players and bots in this squad. Bugfix for Epic's code prior to patch two.
// ============================================================================

function int GetSize()
{
  if (LeaderPRI.bBot)
    return Size;

  return Size + 1;  // plus human leader
}


// ============================================================================
// AddBot
//
// Adds the added bot's enemy to the squad's enemies and retasks the bot. If
// the bot is currently following a scripted sequence, stops it.
// ============================================================================

function AddBot(Bot Bot)
{
  Super.AddBot(Bot);

  Bot.FreeScript();
  TeamPlayerReplicationInfo(Bot.PlayerReplicationInfo).bHolding = False;

  if (Bot.Enemy != None)
    AddEnemy(Bot.Enemy);

  Retask(Bot);
}


// ============================================================================
// RemoveBot
//
// Removes the removed bot's enemy from the squad's enemies unless another bot
// has acquired the same enemy.
// ============================================================================

function RemoveBot(Bot Bot)
{
  local Bot thisBot;

  Super.RemoveBot(Bot);

  if (Bot.Enemy != None) {
    for (thisBot = SquadMembers; thisBot != None; thisBot = thisBot.NextSquadMember)
      if (thisBot.Enemy == Bot.Enemy)
        break;

    if (thisBot == None)
      RemoveEnemy(Bot.Enemy);
  }
}


// ============================================================================
// AddEnemy
//
// Initializes the TInfoEnemy structure for the newly added enemy if this
// squad is ordered for an objective's defense. Also, if the acquired enemy is
// the one this squad hunted for, resets the hunting order.
// ============================================================================

function bool AddEnemy(Pawn PawnEnemy)
{
  local bool bEnemyAdded;
  local int iEnemy;
  local float DistanceObjective;

  bEnemyAdded = Super.AddEnemy(PawnEnemy);

  if (bEnemyAdded) {
    if (GetOrders() == 'Defend') {
      for (iEnemy = 0; iEnemy < ArrayCount(Enemies); iEnemy++)
        if (Enemies[iEnemy] == PawnEnemy)
          break;

      DistanceObjective = Class'JBBotTeam'.Static.CalcDistance(PawnEnemy.Controller, SquadObjective);

      ListInfoEnemy[iEnemy].TimeUpdate        = Level.TimeSeconds;
      ListInfoEnemy[iEnemy].bIsApproaching    = False;
      ListInfoEnemy[iEnemy].bIsVisible        = True;
      ListInfoEnemy[iEnemy].DistanceObjective = DistanceObjective;
    }

    if (PawnEnemy.Controller == InfoHunt.Controller)
      ClearHunt();  // found him, no more hunting needed
  }

  return bEnemyAdded;
}


// ============================================================================
// ModifyThreat
//
// For defending squads, increases the perceived threat posed through an enemy
// player approaching the defended objective and for enemies closer to the
// defended objective than the inquiring bot itself.
// ============================================================================

function float ModifyThreat(float Threat, Pawn PawnThreat, bool bThreatVisible, Bot Bot)
{
  local int iEnemy;
  local float DistanceObjectiveBot;

  if (GetOrders() == 'Defend') {
    for (iEnemy = 0; iEnemy < ArrayCount(Enemies); iEnemy++)
      if (Enemies[iEnemy] == PawnThreat)
        break;

    if (ListInfoEnemy[iEnemy].bIsApproaching &&
       !ListInfoEnemy[iEnemy].bIsVisible)
      Threat += 0.3;

    DistanceObjectiveBot = Class'JBBotTeam'.Static.CalcDistance(Bot, SquadObjective);
    if (DistanceObjectiveBot > ListInfoEnemy[iEnemy].DistanceObjective)
      Threat += 0.1;
  }

  return Super.ModifyThreat(Threat, PawnThreat, bThreatVisible, Bot);
}


// ============================================================================
// MustKeepEnemy
//
// Tells the squad to keep the given enemy by all means if the squad is on
// defense and the enemy is approaching the defended objective.
// ============================================================================

function bool MustKeepEnemy(Pawn PawnEnemy)
{
  local bool bIsVisible;
  local int iEnemy;
  local float DistanceObjective;
  local Bot thisBot;

  if (GetOrders() == 'Defend') {
    for (iEnemy = 0; iEnemy < ArrayCount(Enemies); iEnemy++)
      if (Enemies[iEnemy] == PawnEnemy)
        break;

    if(iEnemy == ArrayCount(Enemies))
      return False;

    if (ListInfoEnemy[iEnemy].TimeUpdate < Level.TimeSeconds + 0.5) {
      DistanceObjective = Class'JBBotTeam'.Static.CalcDistance(PawnEnemy.Controller, SquadObjective);

      for (thisBot = SquadMembers; thisBot != None; thisBot = thisBot.NextSquadMember)
        if (thisBot.Enemy == PawnEnemy &&
            thisBot.CanSee(PawnEnemy))
          bIsVisible = True;

      if (bIsVisible || ListInfoEnemy[iEnemy].bIsVisible)  // came into view, went out of view, or is in view
        ListInfoEnemy[iEnemy].bIsApproaching = (DistanceObjective <= ListInfoEnemy[iEnemy].DistanceObjective);

      ListInfoEnemy[iEnemy].TimeUpdate        = Level.TimeSeconds;
      ListInfoEnemy[iEnemy].bIsVisible        = bIsVisible;
      ListInfoEnemy[iEnemy].DistanceObjective = DistanceObjective;
    }

    if (ListInfoEnemy[iEnemy].bIsApproaching)
      return True;
  }

  return Super.MustKeepEnemy(PawnEnemy);
}


// ============================================================================
// PriorityObjective
//
// If this squad is currently attacking a switch, returns the number of
// players who could be released by a successful attack; otherwise, returns
// zero to indicate no particular priority.
// ============================================================================

function byte PriorityObjective(Bot Bot)
{
  if (SquadObjective != None && JBBotTeam(Team.AI).IsObjectiveAttack(SquadObjective))
    return JBBotTeam(Team.AI).CountPlayersReleasable(SquadObjective);

  return 0;
}


// ============================================================================
// CheckSquadObjectives
//
// If this squad is on a hunt, directs the leader to the hunting target or
// aborts the hunt if the leader has already reached it.
// ============================================================================

function bool CheckSquadObjectives(Bot Bot)
{
  if (Bot.Pawn == None)
    return False;

  if (Super.CheckSquadObjectives(Bot))
    return True;

  if (InfoHunt.NavigationPoint != None && Bot == SquadLeader)
    if (Bot.Pawn.ReachedDestination(InfoHunt.NavigationPoint))
      ClearHunt();
    else
      return FindPathToObjective(Bot, InfoHunt.NavigationPoint);

  return False;
}


// ============================================================================
// GetOrderStringFor
//
// Returns a string describing the given player's current status. Unchanged in
// respect to the function it overrides except that it uses a GameObjective's
// actual ObjectiveName.
// ============================================================================

simulated function string GetOrderStringFor(TeamPlayerReplicationInfo TeamPlayerReplicationInfo)
{
  GetOrders();

  if (CurrentOrders == 'defend') return DefendString @ SquadObjective.ObjectiveName;
  if (CurrentOrders == 'attack') return AttackString @ SquadObjective.ObjectiveName;

  return Super.GetOrderStringFor(TeamPlayerReplicationInfo);
}


// ============================================================================
// Hunt
//
// Sends this squad on a hunt for the given player who was last seen at the
// given location. The hunt ends when the player is found or killed. Returns
// whether the hunt could be started.
// ============================================================================

function bool Hunt(Controller Controller, NavigationPoint NavigationPoint)
{
  if (Bot(SquadLeader) == None)
    return False;

  InfoHunt.Controller      = Controller;
  InfoHunt.NavigationPoint = NavigationPoint;

  return CheckSquadObjectives(Bot(SquadLeader));
}


// ============================================================================
// CanHunt
//
// Checks and returns whether this squad is currently fit to hunt an enemy
// player.
// ============================================================================

function bool CanHunt()
{
  return (TimeInitialized < Level.TimeSeconds - 0.5 &&
          Bot(SquadLeader) != None &&
          GetOrders() == 'Freelance' &&
          CountEnemies() == 0);
}


// ============================================================================
// CanHuntBetterThan
//
// Checks and returns whether this squad is currently more fit to hunt the
// given enemy than the given squad.
// ============================================================================

function bool CanHuntBetterThan(JBBotSquad Squad, Controller Controller)
{
  if (Squad == None)
    return True;

  return (Squad.InfoHunt.Controller != None &&
          Squad.InfoHunt.Controller != Controller &&
          (InfoHunt.Controller == None ||
           InfoHunt.Controller == Controller));
}


// ============================================================================
// IsHunting
//
// Checks and returns whether this squad is currently hunting the given player
// or any player at all if none is specified.
// ============================================================================

function bool IsHunting(optional Controller Controller)
{
  if (Controller == None)
    return (InfoHunt.Controller != None);
  else
    return (InfoHunt.Controller == Controller);
}


// ============================================================================
// ClearHunt
//
// Stops an ongoing hunt.
// ============================================================================

function ClearHunt()
{
  InfoHunt.Controller      = None;
  InfoHunt.NavigationPoint = None;

  if (Bot(SquadLeader) != None)
    CheckSquadObjectives(Bot(SquadLeader));
}


// ============================================================================
// NotifyKilled
//
// If the killed player is the one this squad hunted for, ends the hunt.
// ============================================================================

function NotifyKilled(Controller ControllerKiller, Controller ControllerVictim, Pawn PawnVictim)
{
  if (ControllerVictim == InfoHunt.Controller)
    ClearHunt();

  Super.NotifyKilled(ControllerKiller, ControllerVictim, PawnVictim);
}


// ============================================================================
// StartEvasive
// StopEvasive
//
// Start or stop evasive squad tactics. Works only on squads that are already
// freelancing.
// ============================================================================

function StartEvasive() { if (GetOrders() == 'Freelance' && !IsEvasive()) GotoState('Evasive'); }
function StopEvasive()  { if (                               IsEvasive()) GotoState('');        }


// ============================================================================
// IsEvasive
//
// Checks and returns whether this squad is currently trying to be evasive.
// ============================================================================

function bool IsEvasive()
{
  return IsInState('Evasive');
}


// ============================================================================
// state Evasive
//
// Bots try to avoid enemy encounters and only react aggressively when they're
// cornered by enemies.
// ============================================================================

state Evasive {

  // ================================================================
  // BeginState
  //
  // Puts this squad on freelance.
  // ================================================================

  event BeginState()
  {
    bFreelance       = True;
    bFreelanceAttack = False;
    bFreelanceDefend = False;

    SquadObjective = None;
  }


  // ================================================================
  // CheckSquadObjectives
  //
  // Tries to find nearby cover for the given bot if the bot is
  // aware of an enemy. If no cover is found, makes the bot attack
  // the enemy normally.
  // ================================================================

  function bool CheckSquadObjectives(Bot Bot)
  {
    local int iNavigationPoint;
    local float RatingNavigationPointBest;
    local float RatingNavigationPointCurrent;
    local NavigationPoint NavigationPointBest;
    local NavigationPoint NavigationPointCurrent;
    local NavigationPoint NavigationPointStart;
    local array<NavigationPoint> ListNavigationPointCover;
    local array<NavigationPoint> ListNavigationPointChecked;

    if (Bot.Enemy == None ||
       !Bot.EnemyVisible()) {
      Bot.LoseEnemy();
      return Super.CheckSquadObjectives(Bot);
    }

    NavigationPointStart = NavigationPoint(Bot.MoveTarget);
    if (NavigationPointStart == None)
      return Super.CheckSquadObjectives(Bot);

    if (IsInCover(NavigationPoint(Bot.RouteGoal), Bot.Enemy.Location))
      return Bot.SetRouteToGoal(Bot.RouteGoal);

    FindCover(NavigationPointStart,
              Bot.Pawn.Location,
              Bot.Enemy.Location,
              ListNavigationPointCover,
              ListNavigationPointChecked);

    if (ListNavigationPointCover.Length == 0)
      return Super.CheckSquadObjectives(Bot);

    for (iNavigationPoint = 0; iNavigationPoint < ListNavigationPointCover.Length; iNavigationPoint++) {
      NavigationPointCurrent = ListNavigationPointCover[iNavigationPoint];

      // the closer to the approaching bot and the farther from the acquired enemy, the better
      RatingNavigationPointCurrent = VSize(Bot.Enemy.Location - NavigationPointCurrent.Location) /
                                     VSize(Bot.Pawn .Location - NavigationPointCurrent.Location);

      if (NavigationPointBest == None || RatingNavigationPointCurrent > RatingNavigationPointBest) {
        NavigationPointBest = NavigationPointCurrent;
        RatingNavigationPointBest = RatingNavigationPointCurrent;
      }
    }

    Bot.SetRouteToGoal(NavigationPointBest);
    Bot.SetAttractionState();

    return True;
  }


  // ================================================================
  // FindCover
  //
  // Searches the path network starting from the given point to find
  // points that provide cover from the given enemy's position.
  // ================================================================

  function FindCover(NavigationPoint NavigationPointStart,
                     vector LocationOrigin, vector LocationEnemy,
                     out array<NavigationPoint> ListNavigationPointCover,
                     out array<NavigationPoint> ListNavigationPointChecked) {

    local int iReachSpec;
    local int iNavigationPoint;
    local float DistanceStart;
    local float DistanceTarget;
    local NavigationPoint NavigationPointTarget;

    DistanceStart = VSize(NavigationPointStart.Location - LocationOrigin);
    if (DistanceStart > 4096.0)
      return;

    for (iReachSpec = 0; iReachSpec < NavigationPointStart.PathList.Length; iReachSpec++) {
      NavigationPointTarget = NavigationPointStart.PathList[iReachSpec].End;

      for (iNavigationPoint = 0; iNavigationPoint < ListNavigationPointChecked.Length; iNavigationPoint++)
        if (ListNavigationPointChecked[iNavigationPoint] == NavigationPointTarget)
          return;

      DistanceTarget = VSize(NavigationPointTarget.Location - LocationOrigin);
      if (DistanceTarget <= DistanceStart)
        continue;  // spread search outwards only

      ListNavigationPointChecked[ListNavigationPointChecked.Length] = NavigationPointTarget;

      if (Door(NavigationPointTarget) != None &&
         !Door(NavigationPointTarget).bDoorOpen)
        continue;  // do not run against closed doors

      if (Jailbreak(Level.Game).ContainsActorJail(NavigationPointTarget))
        continue;  // do not run into jail

      if (IsInCover(NavigationPointTarget, LocationEnemy))
        ListNavigationPointCover[ListNavigationPointCover.Length] = NavigationPointTarget;
      else
        FindCover(NavigationPointTarget, LocationOrigin, LocationEnemy, ListNavigationPointCover, ListNavigationPointChecked);
    }
  }


  // ================================================================
  // IsInCover
  //
  // Checks whether the given NavigationPoint provides cover from
  // the given enemy location.
  // ================================================================

  function bool IsInCover(NavigationPoint NavigationPoint, vector LocationEnemy)
  {
    if (Door(NavigationPoint) != None &&
       !Door(NavigationPoint).bDoorOpen)
      return False;

    if (JumpPad(NavigationPoint) != None)
      NavigationPoint = NavigationPoint(JumpPad(NavigationPoint).JumpTarget);

    if (NavigationPoint == None)
      return False;

    return !FastTrace(NavigationPoint.Location, LocationEnemy);
  }


  // ================================================================
  // MustKeepEnemy
  //
  // Tells bots never to keep an enemy in evasive mode.
  // ================================================================

  function bool MustKeepEnemy(Pawn PawnEnemy)
  {
    return False;
  }

} // state Evasive