514 lines
15 KiB
Ucode
514 lines
15 KiB
Ucode
//=============================================================================
|
|
// AICommand_Attack_Melee
|
|
//=============================================================================
|
|
// AI Action for melee attacks
|
|
//=============================================================================
|
|
// Killing Floor 2
|
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
|
//=============================================================================
|
|
|
|
class AICommand_Attack_Melee extends AICommand_SM_Attack
|
|
within KFAIController
|
|
native(AI);
|
|
|
|
/** Renders sphere around NPC when aborting attack, stays visible from initial abort time until command is popped */
|
|
var bool bDebugShowAbortSequence;
|
|
var vector StepAheadLocation;
|
|
var vector CircleDistance;
|
|
var vector CirclePoint;
|
|
var int CircleDir;
|
|
var bool bNewEnemyPending;
|
|
|
|
/*********************************************************************************************
|
|
* Initialization
|
|
**********************************************************************************************/
|
|
|
|
/** Simple constructor that pushes a new instance of the command for the AI */
|
|
static function bool Melee( KFAIController AI, optional Actor InTarget, optional byte InSMFlags )
|
|
{
|
|
local AICommand_Attack_Melee Cmd;
|
|
|
|
if( AI != None )
|
|
{
|
|
Cmd = new(AI) default.class;
|
|
if( Cmd != None )
|
|
{
|
|
// Use melee target if set, in case we're not attacking a real enemy (a door, for example)
|
|
if( InTarget != none )
|
|
{
|
|
Cmd.AttackTarget = InTarget;
|
|
}
|
|
else
|
|
{
|
|
Cmd.AttackTarget = AI.Enemy;
|
|
}
|
|
Cmd.SMFlags = InSMFlags;
|
|
|
|
//Cmd.bSingleAttack = bInSingleAttack;
|
|
AI.PushCommand( Cmd );
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function Pushed()
|
|
{
|
|
super.Pushed();
|
|
if( IsDoingLatentMove() )
|
|
{
|
|
`warn( MyKFPawn@"STILL DOING LATENT MOVE "$MoveTimer );
|
|
}
|
|
AIZeroMovementVariables();
|
|
// Focus = none; // Temp!
|
|
// MyKFPawn.bBlocksNavigation = true;
|
|
MeleeTarget = (KFPawn(AttackTarget)!=none?KFPawn(AttackTarget):KFPawn(Enemy));
|
|
bFinishRotationBeforeAttack = bUseDesiredRotationForMelee;
|
|
AIActionStatus = "Attempting to attack "$AttackTarget;
|
|
}
|
|
|
|
function Popped()
|
|
{
|
|
AIActionStatus = "Finished melee attack";
|
|
super.Popped();
|
|
// MyKFPawn.bBlocksNavigation = false;
|
|
NotifyMeleeAttackFinished();
|
|
MeleeTarget = none;
|
|
AIActionStatus = "Finished attack "$MeleeTarget;
|
|
|
|
if( Pawn != none )
|
|
{
|
|
Pawn.ZeroMovementVariables();
|
|
}
|
|
}
|
|
|
|
event bool NotifyBump( Actor Other, vector HitNormal )
|
|
{
|
|
`AILog( GetFuncName()$" IN MELEE ATTACK", 'PathWarning' );
|
|
DisableBump( 0.25f );
|
|
return true;
|
|
}
|
|
|
|
function Paused( GameAICommand NewCommand )
|
|
{
|
|
`AILog( self$" PAUSED BY "$NewCommand, 'Warning');
|
|
super.Paused( NewCommand );
|
|
}
|
|
|
|
function Resumed( name OldCommandName )
|
|
{
|
|
`AILog( self$" Resumed, OldCommand: "$OldCommandName );
|
|
// Added 11/19 due to hit reactions pausing Melee command
|
|
super.Resumed( OldCommandName );
|
|
if( OldCommandName == 'AICommand_Pause' )
|
|
{
|
|
// Status = 'Failure';
|
|
// PopCommand( self );
|
|
GotoState( 'DelayFailure' );
|
|
}
|
|
}
|
|
|
|
|
|
/*********************************************************************************************
|
|
* Notifications
|
|
**********************************************************************************************/
|
|
|
|
/** Prevent switching enemies during attacks */
|
|
function bool CanChangeEnemy( Pawn NewEnemy )
|
|
{
|
|
// if( CachedChildCommand != None )
|
|
// {
|
|
// return CachedChildCommand.CanChangeEnemy( NewEnemy );
|
|
// }
|
|
|
|
return false;
|
|
}
|
|
|
|
// function bool NotifyPlayerBecameVisible( Pawn VisiblePlayer )
|
|
// {
|
|
// `AILog( GetFuncName()$"() Seen: "$VisiblePlayer$" ignoring event.", 'SeePlayer' );
|
|
// DisableSeePlayer();
|
|
// return false;
|
|
// }
|
|
|
|
/** Notification from controller that enemy has become visible */
|
|
function bool NotifyPlayerBecameVisible( Pawn VisiblePlayer )
|
|
{
|
|
if( VisiblePlayer.IsAliveAndWell() && AttackTarget != none && !AttackTarget.IsA('Pawn') )
|
|
{
|
|
if( ActorReachable(VisiblePlayer) && SetEnemy(VisiblePlayer) )
|
|
{
|
|
bNewEnemyPending = true;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Debug
|
|
**********************************************************************************************/
|
|
|
|
event String GetDumpString()
|
|
{
|
|
if( Enemy != none )
|
|
{
|
|
return Super.GetDumpString()@"AttackTarget:"@AttackTarget$" Enemy: "$Enemy$" Dist: "$VSize( Enemy.Location - Pawn.Location);
|
|
}
|
|
|
|
return Super.GetDumpString()@"No enemy! AttackTarget:"@AttackTarget;
|
|
}
|
|
|
|
function float GetAbortAttackDistanceSq()
|
|
{
|
|
local vector2D AttackRange;
|
|
local int AttackIdx;
|
|
|
|
if( MyKFPawn != none && MyKFPawn.PawnAnimInfo != none && SMFlags != 255 )
|
|
{
|
|
AttackIdx = SMFlags & 15;
|
|
AttackRange = MyKFPawn.PawnAnimInfo.GetAttackRangeByName( MyKFPawn.PawnAnimInfo.Attacks[AttackIdx].Tag );
|
|
return AttackRange.Y * AttackRange.Y;
|
|
}
|
|
|
|
return 57600.f; // (240UU, old default value)
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Special Move State
|
|
********************************************************************************************* */
|
|
|
|
state Command_SpecialMove
|
|
{
|
|
function BeginState( name PreviousStateName )
|
|
{
|
|
//local float DistFromTargetSq, RangeToCheck;
|
|
if( KFDoorActor(AttackTarget) != none && !KFDoorActor(AttackTarget).IsCompletelyOpen() &&
|
|
MyKFPawn.CanDoSpecialMove(SM_MeleeAttackDoor) && MyKFPawn.PawnAnimInfo != none && MyKFPawn.PawnAnimInfo.DoorAttacks.Length > 0 )
|
|
{
|
|
AttackSpecialMove = SM_MeleeAttackDoor;
|
|
}
|
|
|
|
if( MyKFPawn.MeleeAttackHelper == none )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Move ahead a little if enemy isn't a Pawn
|
|
if( AttackTarget != none && !AttackTarget.IsA('Pawn') )
|
|
{
|
|
EnableSeePlayer();
|
|
|
|
}
|
|
}
|
|
|
|
function ContinuedState()
|
|
{
|
|
GotoState( DefaultStartState,, true );
|
|
}
|
|
|
|
function bool ExecuteSpecialMove()
|
|
{
|
|
// local vector DirToEnemy;
|
|
// local vector SocketLocation;
|
|
|
|
// local KFSM_MeleeAttack AttackSM;
|
|
|
|
if( MyKFPawn == None || !MyKFPawn.IsAliveAndWell() || AttackTarget == none || (Pawn(AttackTarget) != none &&
|
|
!Pawn(AttackTarget).IsAliveAndWell()) || (AttackTarget.IsA('KFDoorActor') && KFDoorActor(AttackTarget).IsCompletelyOpen()) )
|
|
{
|
|
`AILog( self$" ExecuteSpecialMove() returning early "$AttackTarget, 'Command_Attack_Melee' );
|
|
return false;
|
|
}
|
|
|
|
`AILog( self$" ExecuteSpecialMove() AttackTarget: "$AttackTarget, 'Command_Attack_Melee' );
|
|
|
|
// Keep rotating toward melee target
|
|
if( AttackTarget != None && Pawn != none && !Pawn.bCrawler )
|
|
{
|
|
SetDesiredRotation( rotator(AttackTarget.Location - Pawn.Location) ); // 5.14 (lock it too?)
|
|
}
|
|
|
|
|
|
// Start the special move after rotating
|
|
if( (Pawn.bCrawler || Pawn.ReachedDesiredRotation()) && AttackTarget != none )
|
|
{
|
|
`AILog( self$" Target in melee range (Dist: "$VSize(AttackTarget.Location - Pawn.Location)$"... do attack", 'Command_Attack_Melee' );
|
|
MyKFPawn.DoSpecialMove( GetSpecialMove(),,, SMFlags );
|
|
return MyKFPawn.SpecialMove == AttackSpecialMove;
|
|
}
|
|
`AILog( self$" ExecuteSpecialMove returning false because ReachedDesiredRotation isn't done? AttackTarget: "$AttackTarget, 'Command_Attack_Melee' );
|
|
return false;
|
|
}
|
|
|
|
/** Melee attack is complete if... */
|
|
function bool IsSpecialMoveComplete()
|
|
{
|
|
local bool bInvalidTarget;
|
|
|
|
//`AILog(GetFuncname()@MyKFPawn@MyKFPawn.bDoingMeleeAttack@MyKFPawn.IsDoingMeleeAttack()@MeleeTarget@isValidMeleeTarget(KFPawn(MeleeTarget)));
|
|
|
|
if ( MyKFPawn == None || AttackTarget == None || AttackTarget.bDeleteMe || (KFDoorActor(AttackTarget) != none ||
|
|
(KFPawn(AttackTarget) != None && (!IsValidAttackTarget((MeleeTarget)) || VSize(AttackTarget.Location - MyKFPawn.Location) > MyKFPawn.MeleeRange))) ||
|
|
(AttackTarget.IsA('KFDoorActor') && KFDoorActor(AttackTarget).IsCompletelyOpen()) )
|
|
{
|
|
bInvalidTarget = true;
|
|
// don't return here as that will cause us to slide if we're in the middle of a finishing move
|
|
}
|
|
|
|
if( MyKFPawn != None )
|
|
{
|
|
if( MyKFPawn.IsDoingMeleeAttack() )
|
|
{
|
|
return false;
|
|
}
|
|
if( !MyKFPawn.IsDoingSpecialMove( GetSpecialMove() ) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if( bInvalidTarget )
|
|
{
|
|
//`AILog( GetFuncName()@"check #2 returning true, MyKFPawn:" @ MyKFPawn@MyKFPawn.IsDoingMeleeAttack()@IsValidMeleeTarget(KFPawn(Enemy)), 'Command_Attack_Melee' );
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
event HandleAICommandSpecialAction()
|
|
{
|
|
//local vector SocketLocation, DirToEnemy;
|
|
|
|
// If not using root motion...
|
|
if( Pawn.Mesh.RootMotionMode != RMM_Accel && Enemy != none && MyKFPawn != None )
|
|
{
|
|
// Keep rotating toward the AttackTarget
|
|
if( bUseDesiredRotationForMelee && AttackTarget != none )
|
|
{
|
|
SetDesiredRotation( rotator(AttackTarget.Location - Pawn.Location) );
|
|
}
|
|
else
|
|
{
|
|
Focus = AttackTarget;
|
|
}
|
|
|
|
//MyKFPawn.Mesh.GetSocketWorldLocationAndRotation( 'FireballSocket', SocketLocation );
|
|
//DirToEnemy = ( CalcAimLocToHit(Enemy.Location, SocketLocation, 1800, 1800, Enemy.Velocity) - SocketLocation);
|
|
//SetDesiredRotation( rotator(DirToEnemy) );
|
|
|
|
// Abort the attack if it's played for at least one second and the target has since moved out of range
|
|
//if( bCanBeAborted && !bAttackAborted && !InMeleeRange(AttackTarget.Location) && !AttackTarget.IsA('KFDoorActor') && TimePlayed > 1.f )
|
|
// (jc) Changed distance check to use AttackTarget instead of enemy 7/15/14
|
|
if( bCanBeAborted && !bAttackAborted && (VSizeSq(Pawn.Location - AttackTarget.Location) > GetAbortAttackDistanceSq()) && !AttackTarget.IsA('KFDoorActor') )
|
|
{
|
|
`AILog( self$" 1 Aborting attack.. AttackTarget: "$AttackTarget$" Dist: "$VSize( AttackTarget.Location - Pawn.Location )$" Speed: "$VSize( Pawn.Velocity ), 'Command_Attack_Melee' );
|
|
if( MyKFPawn.IsDoingSpecialMove( GetSpecialMove() ) )
|
|
{
|
|
if( bDebugShowAbortSequence )
|
|
{
|
|
DrawDebugSphere( Pawn.Location, 32, 16, 255, 255, 0, false );
|
|
}
|
|
UpdateHistoryString( "attack aborted (out of range)" );
|
|
bAttackAborted = true;
|
|
KFSM_MeleeAttack( MyKFPawn.SpecialMoves[ GetSpecialMove() ] ).AbortedByAICommand();
|
|
}
|
|
}
|
|
if( bCanBeAborted && AttackTarget != none && AttackTarget.IsA('KFDoorActor') )
|
|
{
|
|
if( KFDoorActor( AttackTarget ).WeldIntegrity <= 0 || KFDoorActor( AttackTarget ).IsCompletelyOpen() )
|
|
{
|
|
`AILog( self$" 2 Aborting attack.. AttackTarget: "$AttackTarget$" Dist: "$VSize( AttackTarget.Location - Pawn.Location )$" Speed: "$VSize( Pawn.Velocity ), 'Command_Attack_Melee' );
|
|
if( MyKFPawn.IsDoingSpecialMove( GetSpecialMove() ) )
|
|
{
|
|
bAttackAborted = true;
|
|
KFSM_MeleeAttack( MyKFPawn.SpecialMoves[ GetSpecialMove() ] ).AbortedByAICommand();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function bool HandleSpecialMoveEnded()
|
|
{
|
|
// if( MyKFPawn.PawnAnimInfo != none )
|
|
// {
|
|
// if( SMFlags <= MyKFPawn.PawnAnimInfo.Attacks.Length && MyKFPawn.PawnAnimInfo.Attacks[SMFlags].Tag == 'Projectile' && FRand() < 0.24f )
|
|
// {
|
|
// GotoState( GetStateName(), 'TauntEnemy' );
|
|
// return true;
|
|
// }
|
|
// }
|
|
return false;
|
|
}
|
|
|
|
function FinishedSpecialMove()
|
|
{
|
|
`AILog( GetFuncName(), 'Command_Attack_Melee' );
|
|
UpdateLastMeleeTime( EAS_FullBody );
|
|
Status = 'Success';
|
|
|
|
if( HandleSpecialMoveEnded() )
|
|
{
|
|
return;
|
|
}
|
|
if( bSingleAttack )
|
|
{
|
|
EndOfMeleeAttackNotification();
|
|
}
|
|
}
|
|
|
|
TauntEnemy:
|
|
if( IsTimerActive(nameof(SpecialMoveTimeout), self) )
|
|
{
|
|
SetTimer( 15.f, false, nameof(SpecialMoveTimeout), self );
|
|
}
|
|
bCanBeAborted = false;
|
|
Pawn.ZeroMovementVariables();
|
|
MoveTimer = -1.f;
|
|
StopAllLatentMovement();
|
|
class'AICommand_TauntEnemy'.static.Taunt( Outer, KFPawn(Enemy), TAUNT_Standard );
|
|
RouteCache_Empty();
|
|
Sleep( 0.33f );
|
|
Status = 'Success';
|
|
PopCommand( self );
|
|
}
|
|
|
|
function NotifyEnemyChanged( optional Pawn OldEnemy )
|
|
{
|
|
bNewEnemyPending = true;
|
|
}
|
|
|
|
state TestMove
|
|
{
|
|
Begin:
|
|
StepAheadLocation = GetStepAheadLocation();
|
|
MoveTo( GetStepAheadLocation(), AttackTarget,, Pawn.bIsWalking );
|
|
Sleep( 0.f );
|
|
PopState();
|
|
}
|
|
|
|
function CircleTimer ();
|
|
|
|
state Circle
|
|
{
|
|
function PushedState ()
|
|
{
|
|
MyKFPawn.bAllowAccelSmoothing = false;
|
|
super.PushedState ();
|
|
}
|
|
|
|
function PoppedState ()
|
|
{
|
|
super.PoppedState ();
|
|
//MyKFPawn.bAllowAccelSmoothing = true;
|
|
if( Pawn != None )
|
|
{
|
|
Pawn.ZeroMovementVariables();
|
|
}
|
|
}
|
|
|
|
function bool GetCirclePoint()
|
|
{
|
|
local bool bResult;
|
|
local Vector EnemyToPawn;
|
|
local float Dist;
|
|
|
|
EnemyToPawn = Pawn .Location - Enemy.Location ;
|
|
Dist = VSize (EnemyToPawn);
|
|
if( Dist < CircleDistance.X )
|
|
{
|
|
EnemyToPawn *= (CircleDistance .X / Dist);
|
|
}
|
|
if( Dist > CircleDistance. Y )
|
|
{
|
|
EnemyToPawn *= (CircleDistance .Y / Dist);
|
|
}
|
|
|
|
CirclePoint = Enemy .Location + ( EnemyToPawn >> (rot (0,8192,0) * CircleDir));
|
|
|
|
bResult = PointReachable ( CirclePoint );
|
|
|
|
`AILog( GetFuncName()@CirclePoint @Dist@ bResult, 'Command_Attack_Melee' );
|
|
|
|
return bResult ;
|
|
}
|
|
|
|
Begin:
|
|
//debug
|
|
`AILog( self$" BEGIN TAG" @GetStateName(), 'Command_Attack_Melee' );
|
|
|
|
SetTimer( 1.25f + (FRand () * 1.25f), false, nameof( self.CircleTimer ), self );
|
|
while( IsTimerActive( 'CircleTimer', self ) )
|
|
{
|
|
if( GetCirclePoint() )
|
|
{
|
|
SetFocalPoint( Enemy.Location, true );
|
|
MoveTo( CirclePoint, Enemy,, false);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( FRand () < 0.6f && normal( Pawn.Location - Enemy. Location ) dot vector ( Enemy.Rotation ) < 0.94f )
|
|
{
|
|
Goto( 'Begin' );
|
|
}
|
|
|
|
PopCommand( self );
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a location that Best moves out of the way of StepAsideGoal.
|
|
*/
|
|
function vector GetStepAheadLocation()
|
|
{
|
|
local vector X;
|
|
local float StepDist;
|
|
local vector CheckLocation, HitLocation, HitNormal;
|
|
local float DistFromTarget, RangeToCheck;
|
|
|
|
DistFromTarget = VSize( AttackTarget.Location - Pawn.Location );
|
|
RangeToCheck = MyKFPawn.MeleeAttackHelper.GetMeleeRange();
|
|
|
|
StepDist = DistFromTarget - RangeToCheck;
|
|
|
|
//X = Vector(Pawn.Rotation);
|
|
X = (AttackTarget.Location - Pawn.Location);
|
|
|
|
// Ignore vertical component
|
|
X.Z = 0;
|
|
X = Normal(X);
|
|
CheckLocation = Pawn.Location + X * StepDist;
|
|
|
|
StepDist = MaxStepAsideDist;
|
|
|
|
if( PointReachable(CheckLocation) &&
|
|
(Trace(HitLocation, HitNormal, CheckLocation, Pawn.Location, true) == None || VSize(HitLocation - Pawn.Location) > StepDist * 0.5) )
|
|
{
|
|
return CheckLocation;
|
|
}
|
|
|
|
return vect(0,0,0);
|
|
}
|
|
|
|
DefaultProperties
|
|
{
|
|
bSingleAttack=true
|
|
DefaultStartState=Command_SpecialMove
|
|
bIgnoreNotifies=true
|
|
bIgnoreStepAside=true
|
|
bShouldCheckSpecialMove=false // Newest
|
|
PostSpecialMoveSleepTime=0.0f
|
|
PostSpecialMoveAbortedSleepTime=0.3f
|
|
AttackSpecialMove=SM_MeleeAttack
|
|
bZeroPawnAccelWhenPopped=true
|
|
bCanBeAborted=true
|
|
bDebugShowAbortSequence=false
|
|
bAllowedToAttack=false
|
|
CircleDistance=(X=128.f,Y=256.f)
|
|
}
|