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