1
0
KF2-Dev-Scripts/KFGame/Classes/KFMeleeHelperAI.uc

606 lines
21 KiB
Ucode
Raw Normal View History

2020-12-13 15:01:13 +00:00
//=============================================================================
// 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<KFDamageType> 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<SwipeHitActorData> 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<KFDamageType> DamageType;
};
/** Cached damage values */
var array<DelayedMeleeInfo> 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<KFDamageType> 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<KFDamageType> 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<KFDamageType> 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<KFDamageType> 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<KFDamageType> 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<KFDamageType> 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<KFDamageType> 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<KFDamageType> 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<KFDamageType> 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
}