474 lines
16 KiB
Ucode
474 lines
16 KiB
Ucode
|
//=============================================================================
|
||
|
// AICommand_MoveToEnemy
|
||
|
//=============================================================================
|
||
|
// Base command for moving to enemy - will build path and call MoveToGoal
|
||
|
// command to start the movement.
|
||
|
//=============================================================================
|
||
|
// Killing Floor 2
|
||
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
||
|
//=============================================================================
|
||
|
|
||
|
class AICommand_MoveToEnemy extends AICommand
|
||
|
within KFAIController;
|
||
|
|
||
|
var Actor Path, Find;
|
||
|
var float Radius;
|
||
|
/** Number of times remaining we can fail the MoveToGoal before we give up */
|
||
|
var int MoveTriesRemaining;
|
||
|
/** If this is true, we will move all the way to the enemy instead of just to the first goal */
|
||
|
var bool bCompleteMove;
|
||
|
/** How close to get to the enemy (only valid of bCompleteMove is true) */
|
||
|
var float GoalDistance;
|
||
|
/** If enemy gets this far from us we should abandon the move */
|
||
|
var float AbandonDistance;
|
||
|
/** Last location of enemy... if they've moved too much, redo pathing */
|
||
|
var BasedPosition LastEnemyLocation;
|
||
|
/** Are we allowed to fire during this move? */
|
||
|
var bool bAllowedToAttackDuringMove;
|
||
|
/** If the enemy moves more than this distance away from where it was when we pathed, repath */
|
||
|
var float RepathDistance;
|
||
|
/** override for movement focus */
|
||
|
var Actor OverrideFocus;
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Push/Pause/Pop/Init
|
||
|
********************************************************************************************* */
|
||
|
|
||
|
/** Simple constructor that pushes a new instance of the command for the AI */
|
||
|
static function bool MoveToEnemy( KFAIController AI, bool bInCompleteMove, float InGoalDistance, float InAbandonDistance, optional bool bInAllowedToAttackDuringMove=true, optional actor InFocusOverride=none )
|
||
|
{
|
||
|
local AICommand_MoveToEnemy Cmd;
|
||
|
|
||
|
if( AI != None && AI.Pawn != None )
|
||
|
{
|
||
|
Cmd = new(AI) class'AICommand_MoveToEnemy';
|
||
|
if( Cmd != None )
|
||
|
{
|
||
|
Cmd.bCompleteMove = bInCompleteMove;
|
||
|
Cmd.GoalDistance = InGoalDistance;
|
||
|
Cmd.AbandonDistance = InAbandonDistance;
|
||
|
Cmd.bAllowedToAttackDuringMove = bInAllowedToAttackDuringMove;
|
||
|
Cmd.OverrideFocus = InFocusOverride;
|
||
|
AI.PushCommand( Cmd );
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function Pushed()
|
||
|
{
|
||
|
`AILog( self@GetFuncName()$"() current dist to enemy: "$VSize(Enemy.Location - Pawn.Location), 'Command_MoveToEnemy' );
|
||
|
Super.Pushed();
|
||
|
bMovingToEnemy = true;
|
||
|
SprintTimer();
|
||
|
SetTimer( 1.5f + FRand(), false, nameof( SprintTimer ), self );
|
||
|
GotoState( 'Moving' );
|
||
|
}
|
||
|
|
||
|
function Popped()
|
||
|
{
|
||
|
if( Pawn != none && Enemy != none )
|
||
|
{
|
||
|
`AILog( self@GetFuncName()$"() current dist to enemy: "$VSize(Enemy.Location - Pawn.Location), 'Command_MoveToEnemy' );
|
||
|
}
|
||
|
Super.Popped();
|
||
|
`AILog( self@GetFuncName()$"()", 'Command_MoveToEnemy' );
|
||
|
ClearTimer( 'CheckReachedEnemy', self );
|
||
|
ClearTimer( nameof(CheckEnemyMoved), self );
|
||
|
ClearTimer( nameof(self.DirectMoveTimeout),self );
|
||
|
|
||
|
bFailedToMoveToEnemy = ( Status != 'Success' );
|
||
|
bMovingToEnemy = false;
|
||
|
if( bFailedToMoveToEnemy )
|
||
|
{
|
||
|
if( Pawn != none )
|
||
|
UpdateHistoryString( "Popped but failed to move to enemy at "$Pawn.Location$" Time: "$WorldInfo.TimeSeconds );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function Paused( GameAICommand NewCommand )
|
||
|
{
|
||
|
Super.Paused( NewCommand );
|
||
|
if( Enemy != none )
|
||
|
{
|
||
|
`AILog( GetFuncName()$" "$self$" Paused, replaced by "$NewCommand$" [and dist to enemy: "$VSize(Enemy.Location - Pawn.Location)$"]", 'Command_MoveToEnemy' );
|
||
|
}
|
||
|
PauseTimer( true, 'CheckReachedEnemy', self );
|
||
|
PauseTimer( true, nameof(CheckEnemyMoved), self );
|
||
|
PauseTimer( true, nameof(self.DirectMoveTimeout),self );
|
||
|
}
|
||
|
|
||
|
function Resumed( name OldCommandName )
|
||
|
{
|
||
|
Super.Resumed( OldCommandName );
|
||
|
if( Enemy != none )
|
||
|
{
|
||
|
`AILog( GetFuncName()$" "$self$" Resumed, OldCommand: "$OldCommandName$" [and dist to enemy: "$VSize(Enemy.Location - Pawn.Location)$"]", 'Command_MoveToEnemy' );
|
||
|
}
|
||
|
PauseTimer( false, 'CheckReachedEnemy', self );
|
||
|
PauseTimer( false, nameof(CheckEnemyMoved), self );
|
||
|
PauseTimer( false, nameof(self.DirectMoveTimeout),self );
|
||
|
|
||
|
if( !bCompleteMove )
|
||
|
{
|
||
|
`AILog( GetFuncName()$"() popping command because !bCompleteMove on resume", 'Command_MoveToEnemy' );
|
||
|
Status = ChildStatus;
|
||
|
PopCommand( self );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Combat related
|
||
|
********************************************************************************************* */
|
||
|
|
||
|
function bool NotifyPlayerBecameVisible( Pawn VisiblePlayer )
|
||
|
{
|
||
|
if( CachedChildCommand != None )
|
||
|
{
|
||
|
`AILog( GetFuncName()$"() Seen: "$VisiblePlayer$" notifying "$CachedChildCommand$" and letting it handle the event.", 'SeePlayer' );
|
||
|
return CachedChildCommand.NotifyPlayerBecameVisible( VisiblePlayer );
|
||
|
}
|
||
|
`AILog( GetFuncName()$" "$VisiblePlayer$" ignoring this event", 'SeePlayer' );
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Movement & Pathfinding
|
||
|
********************************************************************************************* */
|
||
|
|
||
|
function SprintTimer()
|
||
|
{
|
||
|
if( MyKFPawn != none && MyKFPawn.IsAliveAndWell() && Enemy != none )
|
||
|
{
|
||
|
if( VSize( Pawn.Velocity ) > 0.f )
|
||
|
{
|
||
|
UpdateSprintFrustration();
|
||
|
if( ShouldSprint() || MyKFPawn.bSprintOverride)
|
||
|
{
|
||
|
MyKFPawn.SetSprinting( true );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
MyKFPawn.SetSprinting( false );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SetTimer( 1.5f + FRand(), false, nameof( SprintTimer ), self ); // ToDo: Disable
|
||
|
}
|
||
|
|
||
|
function CheckReachedEnemy()
|
||
|
{
|
||
|
local float DistToEnemySq;
|
||
|
|
||
|
// only abort if we don't have a child, or our child is a move command.. otherwise let it slide
|
||
|
if( ChildCommand == none || (ChildCommand.IsA('AICommand_MoveToGoal') && ChildCommand.ChildCommand == none) )
|
||
|
{
|
||
|
if( Enemy != None || !Enemy.IsAliveAndWell() )
|
||
|
{
|
||
|
DistToEnemySq = VSizeSq(Enemy.Location-Pawn.Location);
|
||
|
`AIlog( GetFuncName()$"() Dist: "$VSize(Enemy.Location-Pawn.Location)$" GoalDistance: "$GoalDistance$" checking IsWithinAttackRange()", 'ReachedEnemy');
|
||
|
|
||
|
if( IsDoingAttackSpecialMove() )
|
||
|
{
|
||
|
`AILog( GetFuncName()$"() IsWithinAttackRange(): I've reached enemy (Dist:"$Sqrt(DistToEnemySq)$") calling AbortCommand", 'ReachedEnemy' );
|
||
|
// if( !MyKFPawn.IsPawnMovingAwayFromMe( Enemy ) )
|
||
|
// {
|
||
|
AbortCommand( self );
|
||
|
// }
|
||
|
}
|
||
|
else if( AbandonDistance > 0.f && DistToEnemySq >= AbandonDistance*AbandonDistance )
|
||
|
{
|
||
|
`AILog( self$" Move took us outside abandon dist...", 'ReachedEnemy' );
|
||
|
AbortCommand( self );
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
`AILog( "Enemy is no longer valid, aborting command", 'ReachedEnemy' );
|
||
|
AbortCommand( self );
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if( ChildCommand == none )
|
||
|
{
|
||
|
`AILog( GetFuncName()$"() skipping reached evaluation because ChildCommand is NONE", 'ReachedEnemy' );
|
||
|
}
|
||
|
else if( ChildCommand.IsA('AICommand_MoveToGoal') && ChildCommand.ChildCommand == none )
|
||
|
{
|
||
|
`AILog( GetFuncName()$"() skipping reached evaluation because ChildCommand is a MoveToGoal command and has no child of its own", 'ReachedEnemy' );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function CheckEnemyMoved()
|
||
|
{
|
||
|
local float DistSq;
|
||
|
|
||
|
`AILog( GetFuncName(), 'Command_MoveToEnemy' );
|
||
|
if(Find != none)
|
||
|
{
|
||
|
DistSq = VSizeSq( Find.Location - BP2Vect(LastEnemyLocation) );
|
||
|
if( DistSq > RepathDistance*RepathDistance && !IsValidDirectMoveGoal(Find) )
|
||
|
{
|
||
|
if(ChildCommand!=none)
|
||
|
{
|
||
|
// if( !MyKFPawn.IsPawnMovingAwayFromMe( Enemy ) )
|
||
|
// {
|
||
|
// MoveTriesRemaining=default.MoveTriesRemaining+1;
|
||
|
// AbortCommand(ChildCommand);
|
||
|
// }
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function DirectMoveTimeout()
|
||
|
{
|
||
|
`AILog( GetFuncName()$"() - not doing anything, though.", 'Command_MoveToEnemy' );
|
||
|
// if( !MyKFPawn.IsPawnMovingAwayFromMe( Enemy ) )
|
||
|
// {
|
||
|
// `AILog("Was moving directly to enemy, but it took too long and we were bCompleteMove=false, aborting");
|
||
|
// AbortCommand(self);
|
||
|
// }
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Debug
|
||
|
********************************************************************************************* */
|
||
|
|
||
|
/** Build debug string */
|
||
|
event String GetDumpString()
|
||
|
{
|
||
|
return Super.GetDumpString()@Enemy@GoalDistance@bCompleteMove;
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Moving to Enemy State
|
||
|
********************************************************************************************* */
|
||
|
|
||
|
function JumpToTarget()
|
||
|
{
|
||
|
local Vector Suggested;
|
||
|
|
||
|
if( Enemy != none )
|
||
|
{
|
||
|
MyKFPawn.SuggestJumpVelocity( Suggested, Enemy.Location, Pawn.Location );
|
||
|
MyKFPawn.Velocity = Suggested;
|
||
|
MoveTarget = Enemy;
|
||
|
//MyKFPawn.Acceleration = vect( 0,0,0 );
|
||
|
MyKFPawn.SetPhysics( PHYS_Falling );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
state Moving `DEBUGSTATE
|
||
|
{
|
||
|
Begin:
|
||
|
if( MyKFPawn.Physics == PHYS_Falling )
|
||
|
{
|
||
|
WaitForLanding();
|
||
|
}
|
||
|
|
||
|
`AILog( "[Begin Label]"@GetSTatename(), 'Command_MoveToEnemy');
|
||
|
// UnMark failure to move
|
||
|
bFailedToMoveToEnemy = false;
|
||
|
|
||
|
if( Enemy == None )
|
||
|
{
|
||
|
SelectEnemy();
|
||
|
}
|
||
|
|
||
|
if( Enemy != none && !IsDoingAttackSpecialMove() && MoveIsInterruptable() )
|
||
|
{
|
||
|
`AILog( "Found valid enemy: "$Enemy, 'Command_MoveToEnemy' );
|
||
|
bReachedMoveGoal = false;
|
||
|
|
||
|
Radius = Pawn.GetCollisionRadius() + Enemy.GetCollisionRadius() + Pawn.MeleeRange + 1.0;
|
||
|
//[NEW] Commented out 10/10
|
||
|
Radius = FMax( 0.f, GoalDistance - Radius ); // subtract because collision radius already considered by ReachedDestination()
|
||
|
Find = Enemy;
|
||
|
Loop:
|
||
|
// If enemy directly reachable
|
||
|
`AILog( "Loop Label "$find, 'Command_MoveToEnemy' );
|
||
|
SetBasedPosition( LastEnemyLocation, Find.Location );
|
||
|
// Is Pawn closer than maximum 'basic' attack tag range?
|
||
|
if( IsDoingAttackSpecialMove() )
|
||
|
{
|
||
|
`AIlog( self$" [Cleaning up and returning Success status] IsWithinAttackRange returned TRUE DIST: "$VSize( Enemy.Location - Pawn.Location )$" <= MeleeAttackHelper range ("$MyKFPawn.MeleeAttackHelper.GetMeleeRange()$")", 'Command_MoveToEnemy' );
|
||
|
ClearTimer( 'CheckReachedEnemy', self );
|
||
|
ClearTimer( nameof(self.DirectMoveTimeout ),self);
|
||
|
ClearTimer( nameof(CheckEnemyMoved ), self);
|
||
|
Status = 'Success';
|
||
|
PopCommand( self );
|
||
|
Stop;
|
||
|
}
|
||
|
else if( IsValidDirectMoveGoal( Find ) )
|
||
|
{
|
||
|
// WIP - Disabled until ready
|
||
|
/*
|
||
|
if( Pawn.bCrawler )
|
||
|
{
|
||
|
MyKFPawn.SetSprinting( true );
|
||
|
MyKFPawn.bIsSprinting = true;
|
||
|
}
|
||
|
if( Pawn.Physics == PHYS_Spider )
|
||
|
{
|
||
|
JumpToTarget();
|
||
|
if( Pawn.Physics == PHYS_Falling )
|
||
|
{
|
||
|
WaitForLanding();
|
||
|
Goto( 'Begin' );
|
||
|
}
|
||
|
}
|
||
|
*/
|
||
|
`AILog( "Enemy "$Find$" - IsValidDirectMoveGoal() returned true, trying direct move...", 'Command_MoveToEnemy' );
|
||
|
SetTimer( 0.1f, true, nameof(CheckEnemyMoved), self);
|
||
|
SetTimer( 0.1f, true, nameof(self.CheckReachedEnemy), self );
|
||
|
|
||
|
if( !bUseNavMesh || !bUsePluginsForMovement )
|
||
|
{
|
||
|
`AILog( self$" Calling SetMoveGoal() for enemy, using offset "$Radius$", with bCanPathFind = false", 'Command_MoveToEnemy' );
|
||
|
SetMoveGoal( Find,, true, Radius,, false,, bAllowedToAttackDuringMove );
|
||
|
}
|
||
|
if( Enemy != none )
|
||
|
{
|
||
|
`AILog( self$" Done moving to goal, distance is now "$VSize(Enemy.Location - Pawn.Location), 'Command_MoveToEnemy' );
|
||
|
}
|
||
|
if( ChildStatus != 'Success' )
|
||
|
{
|
||
|
if( Enemy != none )
|
||
|
{
|
||
|
`AILog( "Child status was not successful, MoveTriesRemaining: "$MoveTriesRemaining$" Enemy Dist: "$VSize(Enemy.Location - Pawn.Location) );
|
||
|
}
|
||
|
if( --MoveTriesRemaining <= 0)
|
||
|
{
|
||
|
`AILog( "Child failed or aborted, and we're out of move tries.. failing", 'Command_MoveToEnemy' );
|
||
|
UpdateHistoryString( "Child failed or aborted, and we're out of move tries at "$WorldInfo.TimeSeconds$"]" );
|
||
|
GotoState( 'DelayFailure' );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Goto( 'Loop' );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ClearTimer( 'CheckReachedEnemy', self );
|
||
|
ClearTimer( nameof(self.DirectMoveTimeout ),self );
|
||
|
ClearTimer( nameof(CheckEnemyMoved), self );
|
||
|
`AILog( "Finished moving to enemy", 'Command_MoveToEnemy' );
|
||
|
Status = 'Success';
|
||
|
Sleep(0.f);
|
||
|
PopCommand( self );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if( Enemy != none )
|
||
|
{
|
||
|
`AILog( "Trying to build path to enemy who is currently "$VSize( Enemy.Location - Pawn.Location )$" units away", 'Command_MoveToEnemy' );
|
||
|
}
|
||
|
if( Pawn.Anchor == none )
|
||
|
{
|
||
|
SetBestAnchor();
|
||
|
}
|
||
|
// Try to find path to enemy
|
||
|
Path = GeneratePathTo( Find,, true );
|
||
|
// If no path available
|
||
|
if( Path == None )
|
||
|
{
|
||
|
`AILog( "Attempt to build path failed, going to DelayFailure state and calling SetFailedPathToEnemy()", 'PathWarning' );
|
||
|
if( MyKFPawn.bCrawler && MyKFPawn.Physics == PHYS_Spider )
|
||
|
{
|
||
|
MyKFPawn.SetPhysics(PHYS_Falling);
|
||
|
WaitForLanding();
|
||
|
}
|
||
|
// Update failed path time
|
||
|
SetFailedPathToEnemy( Enemy );
|
||
|
GotoState( 'DelayFailure' );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if( VSize(Find.Location - Pawn.Location) <= StrikeRange )
|
||
|
{
|
||
|
GotoState( 'DelayFailure' );
|
||
|
}
|
||
|
else if( !bCompleteMove )
|
||
|
{
|
||
|
`AILog( "Built path to enemy, bComplete move was false.. Pawn.Anchor: "$Pawn.Anchor$" Path: "$Path$" AnchorDist: "$VSize(Pawn.Location - Pawn.Anchor.Location), 'Command_MoveToEnemy' );
|
||
|
//if( RouteCache.Length > 0 )
|
||
|
//{
|
||
|
// `log( "Pawn.RouteCache[0]: "$RouteCache[0] );
|
||
|
// `log( "Pawn.RouteCache[1]: "$RouteCache[1] );
|
||
|
// `log( "Pawn.RouteCache[2]: "$RouteCache[2] );
|
||
|
//}
|
||
|
// Move to first path...
|
||
|
// will research for new path when reached first one
|
||
|
// make sure we don't try to move to our anchor
|
||
|
if( Path == Pawn.Anchor )
|
||
|
{
|
||
|
`AILog( "Preventing myself from moving to my anchor ("$Pawn.Anchor$") as my first path goal", 'Command_MoveToEnemy' );
|
||
|
Path = RouteCache[1];
|
||
|
|
||
|
// clip off the end of the routecache
|
||
|
if(RouteCache.length > 2)
|
||
|
{
|
||
|
RouteCache_RemoveIndex(2,RouteCache.length-2);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if(RouteCache.length > 1)
|
||
|
{
|
||
|
RouteCache_RemoveIndex(1,RouteCache.length-1);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Path = Find;
|
||
|
}
|
||
|
SetTimer( 0.5, true, nameof(CheckEnemyMoved), self );
|
||
|
SetTimer( 0.5f, true, nameof(CheckReachedEnemy), self );
|
||
|
if( !bUseNavMesh || !bUsePluginsForMovement )
|
||
|
{
|
||
|
// Have bValidCache true first time, but false after that
|
||
|
`AILog( "Starting move to goal "$Path$" which is currently "$VSize( Path.Location - Pawn.Location )$" units away", 'Command_MoveToEnemy' );
|
||
|
SetMoveGoal( Path,, true,, true,,,bAllowedToAttackDuringMove );
|
||
|
}
|
||
|
// (untested) SetMoveGoal( Path,, true, StrikeRange, true,,,bAllowedToAttackDuringMove );
|
||
|
`AILog( "Finished move to goal "$Path$" which is now "$VSize( Path.Location - Pawn.Location )$" units away", 'Command_MoveToEnemy' );
|
||
|
|
||
|
ClearTimer( 'CheckReachedEnemy', self );
|
||
|
ClearTimer( 'CheckEnemyMoved', self );
|
||
|
|
||
|
if( ChildStatus != 'Success' && --MoveTriesRemaining <= 0 )
|
||
|
{
|
||
|
`AILog( "Going to DelayedFailure state because ChildStatus is failed and MoveTriesRemaining: "$MoveTriesRemaining, 'Command_MoveToENemy' );
|
||
|
GotoState( 'DelayFailure' );
|
||
|
}
|
||
|
Sleep(0.0);
|
||
|
if( Enemy == none )
|
||
|
{
|
||
|
`AILog( "Lost enemy or enemy became invalid, aborting", 'Command_MoveToEnemy' );
|
||
|
AbortCommand( self );
|
||
|
}
|
||
|
Goto( 'Loop' );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
`AILog( "No valid enemy, or not allowed to move.. bailing ValidEnemy?:"@Enemy@"Allowed to move?"@AllowedToMove()@"Move Interruptable?"@MoveIsInterruptable(), 'Command_MoveToEnemy' );
|
||
|
GotoState( 'DelayFailure' );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
defaultproperties
|
||
|
{
|
||
|
MoveTriesRemaining=3
|
||
|
bAllowedToAttack=false
|
||
|
RepathDistance=1224.f
|
||
|
}
|