//============================================================================= // 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 }