//============================================================================= // KFMeleeHelperAI //============================================================================= // Manages melee attack related functionality for AI controlled pawns //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // - Andrew "Strago" Ladenberger //============================================================================= class KFMeleeHelperAI extends KFMeleeHelperBase within Actor native; /** The default damage applied to each melee hit **/ var() float BaseDamage; /** Default DamageType class */ var() const class MyDamageType; /** How much base physics push force an attack will cause */ var() float MomentumTransfer; /** Struct to keep track of the actors we hit with the move and when it happened */ struct native SwipeHitActorData { /** The actor who was hit */ var transient KFPawn HitActor; /** Last time the actor was hit */ var transient float LastHitTime; }; /** List of actors hit by a swipe attack (see UpdateStabbedActors) */ var transient array SwipedActors; /** If set, track all swiped actors to prevent giving damage too frequently */ var bool bTrackSwipeHits; /** Door damage multiplier for player zed (versus) attacks */ var float PlayerDoorDamageMultiplier; /** Swipe FOV used by player controlled zeds */ var float PlayerControlledFOV; /** Scale factor for player controlled melee impact camera shake */ var float MeleeImpactCamScale; /********************************************************************************************* * Player ping compensation *********************************************************************************************/ struct native DelayedMeleeInfo { var Pawn Victim; var int Damage; var float Momentum; var float TimeOfDamage; var class DamageType; }; /** Cached damage values */ var array PendingDamage; /** Upper limit (in ms) for Ping compensation */ var int MaxPingCompensation; /** Artificial fudge factor to find the sweet spot for online play */ var float PingCompensationScale; cpptext { void TickPingCompensation(AWorldInfo* WorldInfo); } /********************************************************************************************* * Common (Public) *********************************************************************************************/ /** * Deals melee damage to a target actor * @param Victim Target actor to recieve damage * @param Damage Damage to apply if this attack is successful * @param HitLocation Location to apply damage to on the target actor * @param MomentumScalar Momentum transfer to apply if this attack is successful * @param DamageType DamageType to apply if this attack is successful */ function ApplyMeleeDamage(Actor Victim, int Damage, optional float InMomentum=1.f, optional class InDamageType, optional vector HitLocation) { local vector HitDirection; local KFPawn_Monster InstigatorPawn; // Make sure we're doing _some_ damage Damage = Max(Damage, 1); InstigatorPawn = KFPawn_Monster(Instigator); if ( InstigatorPawn != none ) { // Notify pawn InstigatorPawn.NotifyMeleeDamageDealt(); // Notify AIC. After TakeDamage so we can review damage results if( InstigatorPawn.MyKFAIC != none ) { InstigatorPawn.MyKFAIC.NotifyMeleeDamageDealt(); } // Apply rally boost damage Damage = InstigatorPawn.GetRallyBoostDamage( Damage ); } // Bump the hit location so that TakeHitInfo will always replicate if ( IsZero(HitLocation) ) { HitLocation = Victim.Location; HitLocation.Z += FRand(); } HitDirection = Normal(HitLocation - Instigator.Location); // After modifiers are applied, ensure damage is never zero Victim.TakeDamage( Damage, Instigator.Controller, HitLocation, HitDirection * InMomentum, InDamageType,, Outer ); // play camera shake, etc... PlayMeleeHitEffects(Victim, HitLocation, HitDirection); `log(Victim$"**** Melee attack! BaseDamage="@BaseDamage@", ModifiedDamage="@Damage, bLogMelee); } /** Simplified version of RateMeleeVictim designed for any actor type */ function bool ShouldDealDamageToEnemy(Actor Target, optional float Range=MaxHitRange) { local Vector VectToEnemy, HitLoc, HitNormal; local Actor HitActor; local TraceHitInfo HitInfo; local vector TraceOffset; // Enemy must not be too far VectToEnemy = Target.Location - Location; if( VSizeSq(VectToEnemy) > Square(Range) ) { return FALSE; } // must be somewhat facing enemy if( Normal(VectToEnemy) dot Vector(Instigator.Rotation) < 0.0f ) { return FALSE; } // Trace world and block iff block actors is set (ignore CanBecomeDynamic actors) HitActor = Trace(HitLoc, HitNormal, Target.Location, Location, false,, HitInfo, TRACEFLAG_Blocking); if( HitActor != None && HitInfo.HitComponent != None && HitInfo.HitComponent.BlockActors && (HitActor.bWorldGeometry || HitActor.bGameRelevant) ) { // Try again at the EyeHeight - Both must fail to obstruct damage TraceOffset = Instigator.BaseEyeHeight * vect(0,0,1); HitActor = Trace(HitLoc, HitNormal, Target.Location + TraceOffset, Location + TraceOffset, false,, HitInfo, TRACEFLAG_Blocking); if( HitActor != None && HitInfo.HitComponent != None && HitInfo.HitComponent.BlockActors && (HitActor.bWorldGeometry || HitActor.bGameRelevant) ) { return FALSE; } } return TRUE; } /********************************************************************************************* * Area Hit Detection *********************************************************************************************/ /** * Do damage to multiple targets around the character * @param Damage Damage to apply if this attack is successful * @param MomentumScalar Momentum transfer to apply if this attack is successful * @param DamageType DamageType to apply if this attack is successful * @param Range Maximum range for hit detection * @param InFOVCosine Maximum FOV for hit detection. If 0, allows any angle (360 degrees) * @param bPlayersOnly If true, skip non-player controlled pawns */ function bool DoAreaImpact( int Damage, optional float MomentumScalar=1.f, optional class DamageType=MyDamageType, optional float Range=MaxHitRange, optional float InFOVCosine=0.f, optional bool bPlayersOnly) { local KFPawn KFP; local bool bFoundHit; // @note - At low ranges CollidingActors (no VisibleCollidingActors) is okay, // but AllPawns is constant and much faster 99% of the time. foreach WorldInfo.AllPawns( class'KFPawn', KFP, Location, Range ) { if ( bPlayersOnly && !KFP.IsHumanControlled() ) continue; if ( RateMeleeVictim(KFP, Location, Location + Normal(Vector(Instigator.Rotation)) * Range, Range, InFOVCosine) > 0.f ) { ApplyMeleeDamage(KFP, Damage, MomentumScalar, DamageType); bFoundHit = true; } } return bFoundHit; } /********************************************************************************************* * Player (non-AI) zed for versus mode *********************************************************************************************/ /** * Default hit detection for player controlled zeds in Versus mode * @return TRUE if Pawn (not world) was hit for use by zed-time */ function bool DoPlayerControlledImpact(int Damage, optional float MomentumScalar=1.f, optional class DamageType=MyDamageType) { local bool bHitPawn; bHitPawn = DoAreaImpact(Damage, MomentumScalar, DamageType,, PlayerControlledFOV); if ( !bHitPawn ) { DoPlayerWorldTrace(Damage, MomentumScalar, MyDamageType); } return bHitPawn; } /** * A simplified forward trace to hit world geometry and other destructibles. * @see also KFMeleeHelperWeapon::DoWeaponInstantTrace()) */ function bool DoPlayerWorldTrace(int Damage, optional float MomentumScalar=1.f, optional class DamageType=MyDamageType) { local vector StartTrace, EndTrace; local vector HitLocation, HitNormal; local Actor HitActor; StartTrace = GetMeleeStartTraceLocation(); EndTrace = StartTrace + vector(GetMeleeAimRotation()) * MaxHitRange; // There is no weapon to trace from, so we added a custom non-recursive trace that hits // actors (e.g. destructibles) and world geoemtry (e.g. doors), but not pawns. HitActor = TraceNoPawns(HitLocation, HitNormal, EndTrace, StartTrace); if( HitActor != None ) { if ( HitActor.bCanBeDamaged && HitActor.IsA('KFDoorActor') ) { Damage *= PlayerDoorDamageMultiplier; } ApplyMeleeDamage(HitActor, Damage, MomentumScalar, DamageType, HitLocation); return true; } return false; } /** * Replicate (serverside hit detection) camera shake for human instigator */ simulated function PlayMeleeHitEffects(Actor Target, vector HitLocation, vector HitDirection, optional bool bShakeInstigatorCamera=true) { Super.PlayMeleeHitEffects(Target, HitLocation, HitDirection); // local player controlled effects if( bShakeInstigatorCamera && Instigator.IsHumanControlled() ) { PlayerController(Instigator.Controller).ClientPlayCameraShake(MeleeImpactCamShake, MeleeImpactCamScale, true, CAPS_UserDefined, rotator(-HitDirection)); } } /********************************************************************************************* * Anim Notify Handling *********************************************************************************************/ /** Notification called from KFAnimNotify_MeleeImpact. */ function MeleeImpactNotify(KFAnimNotify_MeleeImpact Notify) { local KFAIController KFAIC; local float DoorDamageScale; local float MomentumScalar; local bool bDealtDmg; local class CurrentDamageType; if ( Instigator == None || (Instigator.Role < ROLE_Authority && !Instigator.IsLocallyControlled()) ) { return; // AutomonmousProxy in Versus, but doesn't work with CSHD } MomentumScalar = Notify.bCanDoKnockback ? Notify.MomentumTransferScale * MomentumTransfer : 1.f; KFAIC = KFAIController(Instigator.Controller); // TODO: Get more accurate target from MeleeAttack command CurrentDamageType = Notify.CustomDamageType != none ? Notify.CustomDamageType : MyDamageType; if( KFAIC != none && (KFAIC.DoorEnemy != none || KFAIC.ActorEnemy != none) ) { // Scale door damage based on number of attackers if( KFAIC.DoorEnemy != none ) { DoorDamageScale = KFAIC.DoorEnemy.GetAIDoorDamageScale(); } bDealtDmg = CheckEnemyImpact((Notify.DamageScale * BaseDamage) * DoorDamageScale, MomentumScalar, CurrentDamageType); } else if ( Notify.bDoAreaDamage ) { bDealtDmg = DoAreaImpact(Notify.DamageScale * BaseDamage, MomentumScalar, CurrentDamageType); } else if ( Notify.bDoSwipeDamage ) { bDealtDmg = DoSwipeImpact(Notify.DamageScale * BaseDamage, Notify.SwipeDirection, MomentumScalar,,, CurrentDamageType); } else if( Instigator.IsHumanControlled() ) { // Before you ask, yes you can be a human in the "AI" helper. This is a simplification for third person player // zeds in versus mode. For Area/Swipe the same hit detection will work (ish) for both. bDealtDmg = DoPlayerControlledImpact(Notify.DamageScale * BaseDamage, MomentumScalar, CurrentDamageType); } else { bDealtDmg = CheckEnemyImpact(Notify.DamageScale * BaseDamage, MomentumScalar, CurrentDamageType, Notify.AttackReachOverride > 0.f ? Notify.AttackReachOverride : MaxHitRange ); } if ( bDealtDmg && Notify.bCanCauseZedTime ) { KFGameInfo(WorldInfo.Game).DramaticEvent(0.03); } } /** Do damage directly to AI Controller Enemy * @param Damage Damage to apply if this attack is successful * @param MomentumScalar Momentum transfer to apply if this attack is successful * @param DamageType DamageType to apply if this attack is successful * @param Range Maximum range for hit detection */ protected function bool CheckEnemyImpact(int Damage, float MomentumScalar, class InDamageType=MyDamageType, optional float AttackReachOverride=MaxHitRange) { local KFAIController AIC; AIC = KFAIController(Instigator.Controller); if ( AIC != None ) { if ( AIC.Enemy != None && ShouldDealDamageToEnemy(AIC.Enemy, AttackReachOverride) ) { ResolvePawnMeleeDamage(AIC.Enemy, Damage, MomentumScalar, InDamageType); return true; } else if ( AIC.DoorEnemy != None && (VSizeSq(Location - AIC.DoorEnemy.Location) < Square(500)) ) { ApplyMeleeDamage(AIC.DoorEnemy, Damage, MomentumScalar, InDamageType); return true; } else if ( AIC.ActorEnemy != None && (VSizeSq(Location - AIC.ActorEnemy.Location) < Square(500)) ) { // Apply damage to KFDestructibleActors, see KFDestructibleActor.TakeDamage() where there's special // handling to insta-kill the destructible if the attacker is a NPC with the destructible as the // NPC's ActorEnemy. if( KFDestructibleActor(AIC.ActorEnemy) != none ) { ApplyMeleeDamage(AIC.ActorEnemy, Damage, MomentumScalar, InDamageType); return true; } } } return false; } /********************************************************************************************* * Swipe Hit Detection *********************************************************************************************/ /** A modified Cone hit detection that keeps track of all actors that have been hit * @param Damage Damage to apply if this attack is successful * @param SwipeDir Direction to use for hit detection * @param MomentumScalar Momentum transfer to apply if this attack is successful * @param DamageType DamageType to apply if this attack is successful * @param Range Maximum range for hit detection * @param bPlayersOnly If true, skip non-player controlled pawns */ protected function bool DoSwipeImpact( int Damage, optional EPawnOctant SwipeDir=DIR_Forward, optional float MomentumScalar=1.f, optional float Range=MaxHitRange, optional bool bPlayersOnly, optional class InDamageType=MyDamageType) { local Pawn P; local vector HitLoc, HitNorm; local Actor HitActor; local vector ConeDir, ConeStart; local float ConeRange; local bool bFoundHit; // initialize cone collision ConeDir = GetSwipeVector(SwipeDir); ConeStart = Location + vect(0,0,32); ConeRange = Range; // VSize(BladeEnd - BladeStart) if ( bLogMelee ) { // draw swipe trace DrawDebugCone(ConeStart, ConeDir, ConeRange, Acos(0.7071f), Acos(0.7071f), 16, MakeColor(64,64,64,0), TRUE); } // @perf - AllPawns is ~ 10X faster than VisibleCollidingActors, 10X TraceActors, 2X CollidingActors // - Use AllPawns when we only need pawn collision (fixed cost at any range) // - Use CollidingActors when we need to hit pawns and/or world objects // - Use TraceActors sparingly! (may give better results than CollidingActors) // - Avoid VisibleCollidingActors (RateMeleeVictim handles this) foreach WorldInfo.AllPawns( class'Pawn', P, Location, ConeRange ) { if ( P == Instigator || P.bTearOff ) continue; if ( bPlayersOnly && !P.IsHumanControlled() ) continue; // Make sure victim is in FOV if( Normal2D(P.Location - ConeStart) dot ConeDir < 0.7071f ) { `log(GetFuncName()@"rejected:"@P$", dot:"@Normal(P.Location - ConeStart) dot ConeDir, bLogMelee); continue; } // Don't let them melee the target through a wall HitActor = Trace( HitLoc, HitNorm, P.Location + (P.BaseEyeHeight * vect(0,0,1)), Location, false,,, TRACEFLAG_Blocking ); // KAssets block non-actor traces even if they're not world geometry. Double check that what we've actually hit // is really world geometry, or is at least GameRelevant. -MattF if( HitActor != none && HitActor != P && (HitActor.bWorldGeometry || HitActor.bGameRelevant) ) { `log(GetFuncName()@"rejected:"@P$", melee obstruction:"@HitActor, bLogMelee); continue; } ProcessSwipeHit(P, Damage, MomentumScalar, InDamageType); bFoundHit = true; } // For versus players: Do swipe detection normally, then for each forward direction // apply a simple line trace to hit world geometry and destructibles. if ( !bFoundHit && SwipeDir == DIR_Forward && Instigator.IsHumanControlled() ) { DoPlayerWorldTrace(Damage, MomentumScalar, MyDamageType); } if ( bLogMelee && bFoundHit ) { // draw swipe hit DrawDebugCone(ConeStart, ConeDir, ConeRange, Acos(0.7071f), Acos(0.7071f), 16, MakeColor(255,0,0,255), TRUE); } return bFoundHit; } /** Returns hard-coded normal vectors for each 8-way swipe direction */ protected function vector GetSwipeVector(EPawnOctant SwipeDir) { local rotator R; switch (SwipeDir) { case DIR_ForwardLeft: R = rot(0,-8192,0); break; case DIR_Left: R = rot(0,-16384,0); break; case DIR_BackwardLeft: R = rot(0,-24576,0); break; case DIR_ForwardRight: R = rot(0,8192,0); break; case DIR_Backward: R = rot(0,32768,0); break; case DIR_BackwardRight: R = rot(0,24576,0); break; case DIR_Right: R = rot(0,16384,0); break; } return vector(Rotation + R); } /** * DoSwipeFire found a hit, now cause damage to it. * @param A - the actor that was hit in the swipe * @param Damage - amount of damage to apply if successful * @param InHitLoc - Location of hit detection result * @param MomentumScalar - Amount of momentum to apply if successful * @param DamageType - DamageType to apply if successful */ protected function ProcessSwipeHit(Actor A, int Damage, float MomentumScalar, class InDamageType) { local KFPawn Victim; local int ListIdx; Victim = KFPawn(A); if (Victim == None ) { return; } // Track player hits to ignore overlaps in hit detection if ( bTrackSwipeHits && A.IsA('KFPawn_Human') ) { // See if this actor is already in the list ListIdx = SwipedActors.Find('HitActor', Victim); // Not in the list if (ListIdx == INDEX_NONE) { // Add this actor to the list and mark them for damage ListIdx = SwipedActors.Add(1); SwipedActors[ListIdx].HitActor = Victim; SwipedActors[ListIdx].LastHitTime = WorldInfo.TimeSeconds; } // In the list so see if we need to reset the time and do damage else if ( `TimeSince(SwipedActors[ListIdx].LastHitTime) > 0.25f ) { SwipedActors[ListIdx].LastHitTime = WorldInfo.TimeSeconds; } else { //`log("Swipe damage ignored (interval too short)"@A); return; } } ResolvePawnMeleeDamage(Victim, Damage, MomentumScalar, InDamageType); } /********************************************************************************************* * Ping Compensation for Parry/Block *********************************************************************************************/ /** handle ping compensation before dealing damage */ protected function ResolvePawnMeleeDamage(Pawn Victim, int Damage, float Momentum, class InDamageType) { local DelayedMeleeInfo NewDmgInfo; local float RealDeltaSeconds; local float PingCompensation; if ( Instigator.Role < ROLE_Authority ) { return; // how did we get here? } // Do not apply ping compensation to player instigated damage! if ( Instigator.IsHumanControlled() ) { ApplyMeleeDamage(Victim, Damage, Momentum, InDamageType); return; } // If necessary, apply ping compensation if ( Victim.PlayerReplicationInfo != None && Victim.Weapon != None && ClassIsChildOf(Victim.Weapon.Class, class'KFWeap_MeleeBase') ) { // Calculate delay. Half-ping for anim to reach client and half again for parry key press to reach server PingCompensation = Min(Victim.PlayerReplicationInfo.Ping * `PING_SCALE, MaxPingCompensation) / 1000.f; // artifically reduce the delay to balance parry timing vs. AI reaction time PingCompensation *= PingCompensationScale; // "round" (div by 2) to the closest frame on the server RealDeltaSeconds = WorldInfo.DeltaSeconds / WorldInfo.TimeDilation; PingCompensation -= (RealDeltaSeconds / 2.f); if ( PingCompensation > 0 ) { NewDmgInfo.Victim = Victim; NewDmgInfo.Damage = Damage; NewDmgInfo.Momentum = Momentum; NewDmgInfo.TimeOfDamage = WorldInfo.RealTimeSeconds + PingCompensation; NewDmgInfo.DamageType = InDamageType; PendingDamage.AddItem(NewDmgInfo); return; } } ApplyMeleeDamage(Victim, Damage, Momentum, InDamageType); } /** Called from C++ when ping compensation expires */ event ApplyDelayedPawnDamage(int i) { local KFGameReplicationInfo KFGRI; // If the damage dealer is now dead, don't allow damage after trader/respawn. if ( !Instigator.IsAliveAndWell() ) { KFGRI = KFGameReplicationInfo(WorldInfo.GRI); if ( KFGRI != None && KFGRI.bTraderIsOpen ) { return; } } ApplyMeleeDamage(PendingDamage[i].Victim, PendingDamage[i].Damage, PendingDamage[i].Momentum, PendingDamage[i].DamageType); } defaultproperties { DefaultFOVCosine=0.f MyDamageType=class'KFDT_Slashing' bTrackSwipeHits=true MaxPingCompensation=200 PingCompensationScale=0.5f PlayerControlledFOV=0.05f PlayerDoorDamageMultiplier=10.0f MeleeImpactCamScale=1.f }