2020-12-13 15:01:13 +00:00
|
|
|
//=============================================================================
|
|
|
|
// KFMeleeHelperWeapon
|
|
|
|
//=============================================================================
|
|
|
|
// Manages melee attack related functionality for 1st person weapons
|
|
|
|
//=============================================================================
|
|
|
|
// Killing Floor 2
|
|
|
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
|
|
|
//=============================================================================
|
|
|
|
class KFMeleeHelperWeapon extends KFMeleeHelperBase within KFWeapon
|
|
|
|
config(Game);
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* Directional Attacks
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
|
|
|
/** If set, plays 8-way directional animations */
|
|
|
|
var() bool bUseDirectionalMelee;
|
|
|
|
/** Count each time ChooseBestAttack is called. Used to alternate direction */
|
|
|
|
var transient byte ChooseAtkCount;
|
|
|
|
|
|
|
|
enum EMeleeAttackType
|
|
|
|
{
|
|
|
|
ATK_Normal,
|
|
|
|
ATK_Combo,
|
|
|
|
ATK_DrawStrike,
|
|
|
|
};
|
|
|
|
|
|
|
|
/** Attack settings that should be initialized before calling BeginMeleeAttack */
|
2021-06-02 20:06:18 +00:00
|
|
|
var protected EPawnOctant NextAttackDir;
|
|
|
|
var protected EMeleeAttackType NextAttackType;
|
2020-12-13 15:01:13 +00:00
|
|
|
|
|
|
|
/** Direction of the last played attack */
|
|
|
|
var EPawnOctant CurrentAttackDir;
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* Combo (aka Chain) Attacks
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
|
|
|
/** attack anim variant settings */
|
|
|
|
var() bool bHasChainAttacks;
|
|
|
|
|
|
|
|
/** 4-way directional combo chain sequence */
|
|
|
|
var() array<EPawnOctant> ChainSequence_F;
|
|
|
|
var() array<EPawnOctant> ChainSequence_B;
|
|
|
|
var() array<EPawnOctant> ChainSequence_L;
|
|
|
|
var() array<EPawnOctant> ChainSequence_R;
|
|
|
|
|
|
|
|
/** Cached chain attack sequence currently in use */
|
|
|
|
var array<EPawnOctant> AttackChainSequence;
|
|
|
|
/** Index into the currently playing attack chain sequence */
|
|
|
|
var byte CurrentAtkChainIdx;
|
|
|
|
/** Number of attacks played in this sequence */
|
|
|
|
var int NumChainedAttacks;
|
|
|
|
|
|
|
|
/** Last time MeleeAnimTimer was called */
|
|
|
|
var transient float LastMeleeAnimEnd_ActorTime;
|
|
|
|
/** Set when InitAttackSequence is called during an attack to change direction of the next attack */
|
|
|
|
var transient bool bResetChainSequence;
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* Notify & Timing
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
|
|
|
/** Should use timer instead of anim notifies for melee hits? */
|
|
|
|
var() bool bUseMeleeHitTimer;
|
|
|
|
|
|
|
|
/** Time (after melee starts) before traces for impact start occuring */
|
|
|
|
var() float InitialImpactDelay;
|
|
|
|
/** Time (after impact checks start) to retry impacts */
|
|
|
|
var() float ImpactRetryDuration;
|
|
|
|
/** World time to stop retrying traces for melee impact */
|
|
|
|
var() float ImpactComplete_ActorTime;
|
|
|
|
|
|
|
|
/** Stores whether or not an impact has happened this attack */
|
|
|
|
var bool bHitEnemyThisAttack;
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* Animation / FX
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
|
|
|
/** Scale animation playrate by fatigue level */
|
|
|
|
var InterpCurveFloat FatigueCurve;
|
|
|
|
|
|
|
|
/** Scale parameter for ClientPlayCameraShake */
|
|
|
|
var float MeleeImpactCamShakeScale;
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* Anim Notify / Impact Timing
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
2020-12-13 15:09:05 +00:00
|
|
|
simulated function EPawnOctant GetNextAttackDir()
|
|
|
|
{
|
|
|
|
return NextAttackDir;
|
|
|
|
}
|
|
|
|
|
|
|
|
simulated function SetNextAttackDir(EPawnOctant _nextAttackDir)
|
|
|
|
{
|
|
|
|
NextAttackDir = _nextAttackDir;
|
|
|
|
}
|
|
|
|
|
2020-12-13 15:01:13 +00:00
|
|
|
/** Notification called from KFAnimNotify_MeleeImpact. */
|
|
|
|
simulated function MeleeImpactNotify(KFAnimNotify_MeleeImpact_1P Notify)
|
|
|
|
{
|
|
|
|
local bool bResult;
|
|
|
|
|
|
|
|
if ( !bHasAlreadyHit && !bUseMeleeHitTimer )
|
|
|
|
{
|
|
|
|
// Check impacts for players first
|
|
|
|
bResult = MeleeAttackImpact();
|
|
|
|
// If no impact with player occurs
|
|
|
|
if( !bResult && Instigator.Role == ROLE_Authority )
|
|
|
|
{
|
|
|
|
// Check for impact with world/destructibles
|
|
|
|
bResult = MeleeAttackDestructibles();
|
|
|
|
}
|
|
|
|
|
|
|
|
bHasAlreadyHit = bResult;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function handles checking for all types of melee impacts
|
|
|
|
*/
|
|
|
|
simulated function bool MeleeImpactTimer()
|
|
|
|
{
|
|
|
|
local bool bResult;
|
|
|
|
|
|
|
|
if ( bUseMeleeHitTimer )
|
|
|
|
{
|
|
|
|
// If this is the start first check of a melee attack
|
|
|
|
if( ImpactComplete_ActorTime < 0.f )
|
|
|
|
{
|
|
|
|
// Update the window of time to repeat checks for impact
|
|
|
|
ImpactComplete_ActorTime = GetActorTimeSeconds() + ImpactRetryDuration;
|
|
|
|
}
|
|
|
|
// If window has expired - FAIL
|
|
|
|
if( GetActorTimeSeconds() > ImpactComplete_ActorTime )
|
|
|
|
{
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check impacts for players first
|
|
|
|
bResult = MeleeAttackImpact();
|
|
|
|
// If no impact with player occurs
|
|
|
|
if( !bResult && Instigator.Role == ROLE_Authority )
|
|
|
|
{
|
|
|
|
// Check for impact with world/destructibles
|
|
|
|
bResult = MeleeAttackDestructibles();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no impact has occured
|
|
|
|
if( !bResult && bUseMeleeHitTimer )
|
|
|
|
{
|
|
|
|
// Set timer to retry impact
|
|
|
|
SetTimer(0.1, FALSE, nameof(MeleeImpactTimer), self);
|
|
|
|
}
|
|
|
|
|
|
|
|
return bResult;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Main hit detection and resolution for melee attacks
|
|
|
|
* Network: Local Player
|
|
|
|
*/
|
|
|
|
simulated function bool MeleeAttackImpact()
|
|
|
|
{
|
|
|
|
local Array<ImpactInfo> ImpactList;
|
|
|
|
local int Idx;
|
|
|
|
//local ImpactInfo RealImpact;
|
|
|
|
local vector StartTrace, EndTrace;
|
|
|
|
local bool bHasAnyHit;
|
|
|
|
|
|
|
|
// local player only for clientside hit detection
|
|
|
|
if ( Instigator == None || !Instigator.IsLocallyControlled() )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
StartTrace = GetMeleeStartTraceLocation();
|
|
|
|
EndTrace = StartTrace + vector(GetMeleeAimRotation()) * MaxHitRange;
|
|
|
|
|
|
|
|
CalcWeaponMeleeAttack(StartTrace, EndTrace, ImpactList);
|
|
|
|
bHasAnyHit = ImpactList.length > 0;
|
|
|
|
|
|
|
|
for (Idx = 0; Idx < ImpactList.Length; Idx++)
|
|
|
|
{
|
|
|
|
ProcessMeleeHit(CurrentFireMode, ImpactList[Idx]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( Instigator.Role < ROLE_Authority )
|
|
|
|
{
|
|
|
|
SendClientImpactList(CurrentFireMode, ImpactList);
|
|
|
|
}
|
|
|
|
|
|
|
|
return bHasAnyHit;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* FOV (aka Area, Cone) Hit Detection
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* CalcWeaponMeleeAttack: Simulate an instant hit melee attack.
|
|
|
|
* This doesn't deal any damage nor trigger any effect. It just simulates a shot and returns
|
|
|
|
* the hit information, to be post-processed later.
|
|
|
|
*/
|
|
|
|
simulated function ImpactInfo CalcWeaponMeleeAttack(vector StartTrace, vector EndTrace, optional out array<ImpactInfo> ImpactList, optional vector Extent)
|
|
|
|
{
|
|
|
|
local KFPawn KFPOwner;
|
|
|
|
local KFSM_InteractionPawnFollower FollowerSM;
|
|
|
|
local Pawn BestVictim;
|
|
|
|
local ImpactInfo CurrentImpact;
|
|
|
|
local array<Pawn> VictimList;
|
|
|
|
local vector RayDir;
|
|
|
|
|
|
|
|
// nudge impact direction (momentum) based on attack type
|
|
|
|
RayDir = GetAdjustedRayDir(EndTrace - StartTrace);
|
|
|
|
|
|
|
|
// If an enemy pawn has engaged us in an interaction special move, try to target them first
|
|
|
|
KFPOwner = KFPawn( Instigator );
|
|
|
|
if( KFPOwner.IsDoingSpecialMove() )
|
|
|
|
{
|
|
|
|
FollowerSM = KFSM_InteractionPawnFollower( KFPOwner.SpecialMoves[KFPOwner.SpecialMove] );
|
|
|
|
if( FollowerSM != none
|
|
|
|
&& FollowerSM.Leader != none
|
|
|
|
&& RateMeleeVictim(FollowerSM.Leader, StartTrace, EndTrace, MaxHitRange, DefaultFOVCosine) > -1.f )
|
|
|
|
{
|
|
|
|
BestVictim = FollowerSM.Leader;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find nearby pawn targets - in the future we may want to calculate multiple victim impacts
|
|
|
|
if( BestVictim == none )
|
|
|
|
{
|
|
|
|
BestVictim = FindVictimByFOV(StartTrace, EndTrace);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( BestVictim != None )
|
|
|
|
{
|
|
|
|
VictimList[0] = BestVictim;
|
|
|
|
CalcVictimImpactList(VictimList, StartTrace, EndTrace, RayDir, ImpactList);
|
|
|
|
}
|
|
|
|
else if ( Instigator.Weapon != None )
|
|
|
|
{
|
|
|
|
DoWeaponInstantTrace(StartTrace, EndTrace, CurrentImpact);
|
|
|
|
if( CurrentImpact.HitActor != None )
|
|
|
|
{
|
|
|
|
// Check for world geometry or CanBeDamaged (e.g. ragdolls, destructibles)
|
|
|
|
if ( CurrentImpact.HitActor == WorldInfo
|
|
|
|
|| CurrentImpact.HitActor.bBlockActors
|
|
|
|
|| CurrentImpact.HitActor.bCanBeDamaged )
|
|
|
|
{
|
|
|
|
CurrentImpact.RayDir = RayDir;
|
|
|
|
ImpactList[ImpactList.Length] = CurrentImpact;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
`log(WorldInfo.TimeSeconds @ GetFuncName() @ "HitWallCheck, HitActor:" @ CurrentImpact.HitActor, bLogMelee && CurrentImpact.HitActor != None);
|
|
|
|
}
|
|
|
|
|
|
|
|
if( bLogMelee )
|
|
|
|
{
|
|
|
|
DrawDebugLine(StartTrace, EndTrace, 128, 0, 0, TRUE);
|
|
|
|
DrawDebugLine(StartTrace, StartTrace + RayDir * 100, 0, 128, 0, TRUE);
|
|
|
|
// while debugging melee flush the lines after some inactivity
|
|
|
|
SetTimer(10.f, false, nameof(FlushPersistentDebugLines));
|
|
|
|
}
|
|
|
|
|
|
|
|
return CurrentImpact;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Converts a KFPawn list into a ImpactInfo list */
|
|
|
|
simulated function CalcVictimImpactList(array<Pawn> VictimList, vector StartTrace, vector EndTrace, vector RayDir, optional out array<ImpactInfo> ImpactList)
|
|
|
|
{
|
|
|
|
local ImpactInfo HitZoneImpact;
|
|
|
|
local int i;
|
|
|
|
|
|
|
|
for ( i = 0; i < VictimList.Length; i++ )
|
|
|
|
{
|
|
|
|
if ( TraceMeleeAttackHitZones(VictimList[i], StartTrace, EndTrace, HitZoneImpact) )
|
|
|
|
{
|
|
|
|
HitZoneImpact.RayDir = RayDir; // not set by TraceAllPhysicsAssetInteractions
|
|
|
|
ImpactList[ImpactList.Length] = HitZoneImpact;
|
|
|
|
`log(GetFuncName()@"HitZone:"$ImpactList[0].HitInfo.BoneName@"DmgScale:"$VSize(RayDir)@"HitActor:"$VictimList[i], bLogMelee);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
`log(GetFuncName()@"HitVictimCheck missed all hit zones"); // should never happen
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Calculate a new Impact RayDir for a given AttackType. Used by FOV-style
|
|
|
|
* collision to simulate directional attack momentum.
|
|
|
|
*/
|
|
|
|
simulated function vector GetAdjustedRayDir(vector ImpactRayDir)
|
|
|
|
{
|
|
|
|
local rotator R;
|
|
|
|
|
|
|
|
if ( bUseDirectionalMelee )
|
|
|
|
{
|
|
|
|
switch ( CurrentAttackDir )
|
|
|
|
{
|
|
|
|
case DIR_Forward: R = rot(-8192, 0, 0); break;
|
|
|
|
case DIR_ForwardLeft: R = rot(-8192,-8192, 0); break;
|
|
|
|
case DIR_ForwardRight: R = rot(-8192, 8192, 0); break;
|
|
|
|
case DIR_Backward: R = rot( 8192, 0, 0); break;
|
|
|
|
case DIR_BackwardLeft: R = rot( 8192,-8192, 0); break;
|
|
|
|
case DIR_BackwardRight: R = rot( 8192, 8192, 0); break;
|
|
|
|
case DIR_Left: R = rot( 0, -8192, 0); break;
|
|
|
|
case DIR_Right: R = rot( 0, 8192, 0); break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return vector(rotator(ImpactRayDir) + R);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Normal(ImpactRayDir);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* Hitbox Collision Detection
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
|
|
|
/** Recieve collision event and register impact with CSHD */
|
|
|
|
event ProcessHitboxCollision(Actor HitActor, vector StartTrace, vector EndTrace, vector HitLocation, vector HitNormal, const out TraceHitInfo HitInfo, optional out ImpactInfo Impact)
|
|
|
|
{
|
|
|
|
local Array<ImpactInfo> ImpactList;
|
|
|
|
local ImpactInfo HitZoneImpact;
|
|
|
|
local int Idx;
|
|
|
|
|
|
|
|
if ( Instigator != None && Instigator.IsLocallyControlled() )
|
|
|
|
{
|
|
|
|
Super.ProcessHitboxCollision(HitActor, StartTrace, EndTrace, HitLocation, HitNormal, HitInfo, Impact);
|
|
|
|
ImpactList[0] = Impact;
|
|
|
|
|
|
|
|
// Trace physics asset hit zones
|
|
|
|
if ( HitActor.IsA('KFPawn') )
|
|
|
|
{
|
|
|
|
if ( TraceMeleeAttackHitZones(KFPawn(HitActor), StartTrace, EndTrace, HitZoneImpact, HitInfo.BoneName) )
|
|
|
|
{
|
|
|
|
HitZoneImpact.RayDir = Impact.RayDir; // TraceAllPhysicsAssetInteractions doesn't set RayDir
|
|
|
|
ImpactList[0] = HitZoneImpact;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
`log(GetFuncName()@"missed hit zone... continuing with hitbox TraceInfo");
|
|
|
|
}
|
|
|
|
|
|
|
|
// encode damage scale based on fan collision into the raydir
|
|
|
|
ImpactList[0].RayDir *= GetDamageScaleByAngle(HitLocation);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (Idx = 0; Idx < ImpactList.Length; Idx++)
|
|
|
|
{
|
|
|
|
ProcessMeleeHit(CurrentFireMode, ImpactList[Idx]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( Instigator.Role < ROLE_Authority )
|
|
|
|
{
|
|
|
|
SendClientImpactList(CurrentFireMode, ImpactList);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Called when a hitbox notify starts to allow script to perform a custom world trace */
|
|
|
|
event InitWorldTraceForHitboxCollision()
|
|
|
|
{
|
|
|
|
SetTimer(0.1, false, nameof(HitboxSimpleWorldTrace), self);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Performs a simple forward trace if bHitboxPawnsOnly=TRUE */
|
|
|
|
function bool HitboxSimpleWorldTrace()
|
|
|
|
{
|
|
|
|
local vector StartTrace, EndTrace;
|
|
|
|
local ImpactInfo CurrentImpact;
|
|
|
|
local array<ImpactInfo> ImpactList;
|
|
|
|
local int Idx;
|
|
|
|
|
|
|
|
// local player only for clientside hit detection
|
|
|
|
if ( Instigator == None || !Instigator.IsLocallyControlled() )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
StartTrace = GetMeleeStartTraceLocation();
|
|
|
|
EndTrace = StartTrace + vector(GetMeleeAimRotation()) * MaxHitRange;
|
|
|
|
|
|
|
|
DoWeaponInstantTrace(StartTrace, EndTrace, CurrentImpact);
|
|
|
|
if( CurrentImpact.HitActor != None )
|
|
|
|
{
|
|
|
|
// Check for world geometry and other destructibles
|
|
|
|
if ( CurrentImpact.HitActor.bWorldGeometry || (CurrentImpact.HitActor.bCanBeDamaged && !CurrentImpact.HitActor.IsA('Pawn')) )
|
|
|
|
{
|
|
|
|
CurrentImpact.RayDir = GetAdjustedRayDir(EndTrace - StartTrace);
|
|
|
|
ImpactList[ImpactList.Length] = CurrentImpact;
|
|
|
|
|
|
|
|
for (Idx = 0; Idx < ImpactList.Length; Idx++)
|
|
|
|
{
|
|
|
|
ProcessMeleeHit(CurrentFireMode, ImpactList[Idx]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( Instigator.Role < ROLE_Authority )
|
|
|
|
{
|
|
|
|
SendClientImpactList(CurrentFireMode, ImpactList);
|
|
|
|
}
|
|
|
|
|
|
|
|
`log(GetFuncName()@CurrentImpact.HitActor, bLogMelee);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* Weapon melee state (e.g. MeleeAttackBasic) animation and timing
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
|
|
|
/** Called from state MeleeAtacking */
|
|
|
|
simulated function BeginMeleeAttack(optional bool bIsChainAttack)
|
|
|
|
{
|
|
|
|
local Pawn P;
|
|
|
|
local KFPawn_Monster KFPM;
|
|
|
|
local vector Projection;
|
|
|
|
local float MeleeDuration;
|
|
|
|
|
|
|
|
// Don't let a weak zed grab us when we just melee attacked
|
|
|
|
// Ramm - commented out, too exploitable
|
|
|
|
// SetWeakZedGrabCooldownOnPawn(0.5);
|
|
|
|
|
|
|
|
// initialize attack settings
|
|
|
|
bHasAlreadyHit = false;
|
|
|
|
bHitEnemyThisAttack = false;
|
|
|
|
|
|
|
|
// Let the playercontroller know we did an attack
|
|
|
|
if ( Instigator != None && Instigator.Controller != none &&
|
|
|
|
KFPlayerController(Instigator.Controller) != none )
|
|
|
|
{
|
|
|
|
KFPlayerController(Instigator.Controller).AddShotsFired(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear reset flag (see InitAttackSequence, ContinueMeleeAttack)
|
|
|
|
bResetChainSequence = false;
|
|
|
|
|
|
|
|
// save the direction of this attack
|
|
|
|
CurrentAttackDir = (CurrentFireMode == BASH_FIREMODE || CurrentFireMode == CUSTOM_FIREMODE ) ? DIR_None : NextAttackDir;
|
|
|
|
|
|
|
|
// Notify enemy pawns of melee strike
|
|
|
|
if( WorldInfo.NetMode != NM_Client )
|
|
|
|
{
|
|
|
|
foreach WorldInfo.AllPawns( class'Pawn', P )
|
|
|
|
{
|
|
|
|
if( P.GetTeamNum() != Instigator.GetTeamNum() && P.IsAliveAndWell() && !P.IsHumanControlled() )
|
|
|
|
{
|
|
|
|
Projection = Instigator.Location - P.Location;
|
|
|
|
if( VSizeSQ(Projection) <= Square(MaxHitRange + P.CylinderComponent.CollisionRadius) )
|
|
|
|
{
|
|
|
|
KFPM = KFPawn_Monster( P );
|
|
|
|
if( KFPM != none && KFPM.MyKFAIC != none )
|
|
|
|
{
|
|
|
|
KFPM.MyKFAIC.ReceiveMeleeWarning( CurrentAttackDir, Projection, Instigator );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Select and play attack animation
|
|
|
|
MeleeDuration = PlayMeleeAttackAnimation();
|
|
|
|
|
|
|
|
if ( MeleeDuration > 0.f )
|
|
|
|
{
|
|
|
|
// @hack: Add current DeltaSeconds to timer. This fixes an off-by-one frame issue that can
|
|
|
|
// that can cause the timer to interrupt animation at low framerate. This is particularly
|
|
|
|
// bad for melee when using impact anim notifies.
|
|
|
|
SetTimer(MeleeDuration + WorldInfo.DeltaSeconds, FALSE, nameof(MeleeCheckTimer), self);
|
|
|
|
|
|
|
|
// set the timer to check for hits
|
|
|
|
if( bUseMeleeHitTimer && InitialImpactDelay > 0.f )
|
|
|
|
{
|
|
|
|
// Clear complete time so it is intialized properly
|
|
|
|
ImpactComplete_ActorTime = -1.f;
|
|
|
|
SetTimer(InitialImpactDelay, FALSE, nameof(MeleeImpactTimer), self);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
`warn("MeleeDuration is zero!!!");
|
|
|
|
SetTimer(0.001, false, nameof(HandleFinishedFiring));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Plays the melee attack animations and returns the interrupt time */
|
|
|
|
simulated function float PlayMeleeAttackAnimation()
|
|
|
|
{
|
|
|
|
local name MeleeAnimName;
|
|
|
|
local float InterruptTime, Rate;
|
|
|
|
|
|
|
|
// Calc Reload Duration
|
|
|
|
MeleeAnimName = GetMeleeAnimName(NextAttackDir, NextAttackType);
|
|
|
|
|
|
|
|
InterruptTime = MySkelMesh.GetAnimInterruptTime(MeleeAnimName);
|
|
|
|
Rate = EvalInterpCurveFloat(FatigueCurve, NumChainedAttacks);
|
|
|
|
|
|
|
|
if ( InterruptTime > 0.f )
|
|
|
|
{
|
|
|
|
// Call back into weapon (rather than direct to 'PlayAnimation') so it can choose to override
|
|
|
|
PlayMeleeAnimation(MeleeAnimName, Rate, GetMeleeBlendInTime());
|
|
|
|
}
|
|
|
|
|
|
|
|
return InterruptTime * Rate;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Returns the desired blend in time for this melee attack animation */
|
|
|
|
simulated function float GetMeleeBlendInTime()
|
|
|
|
{
|
|
|
|
// Chain attack anims go offscreen, so we need to disable blending for the next action
|
|
|
|
if ( bHasChainAttacks )
|
|
|
|
{
|
|
|
|
// This can sometimes happen when AtkType != MAT_Combo. LastRenderTime will catch it most of the time.
|
|
|
|
if ( NextAttackType == ATK_Combo || `TimeSince(LastRenderTime) > 0.f )
|
|
|
|
{
|
|
|
|
return 0.f;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0.1f;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the type of melee attack we want to perform
|
|
|
|
* Network: Local Player
|
|
|
|
*/
|
|
|
|
simulated function EPawnOctant ChooseAttackDir()
|
|
|
|
{
|
|
|
|
local vector MoveDir;
|
|
|
|
local EPawnOctant AttackDir;
|
|
|
|
|
|
|
|
ChooseAtkCount++;
|
|
|
|
|
|
|
|
// Prefer to use acceleration, but if the player only has velocity use that.
|
|
|
|
// This allows the player to let go off the movement keys just before an attack.
|
|
|
|
MoveDir = (IsZero(Instigator.Acceleration)) ? Instigator.Velocity : Instigator.Acceleration;
|
|
|
|
|
|
|
|
if ( Instigator == None || !bUseDirectionalMelee || IsZero(MoveDir) )
|
|
|
|
{
|
|
|
|
if ( bUseDirectionalMelee )
|
|
|
|
{
|
|
|
|
// alternate left and right
|
|
|
|
return ((ChooseAtkCount & 1) > 0) ? DIR_Left : DIR_Right;
|
|
|
|
}
|
|
|
|
|
|
|
|
return DIR_None;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get desired attack direction. This is reasonbly close on server/client, but it could still use
|
|
|
|
// a little work to sync perfectly. To see why run with pktlag and spin around like a crazy person.
|
|
|
|
AttackDir = class'KFPawn'.static.CalcQuadRegion(Instigator.GetViewRotation(), MoveDir);
|
|
|
|
|
|
|
|
// reverse left/right so that we're moving in the direction of the attack
|
|
|
|
switch ( AttackDir )
|
|
|
|
{
|
|
|
|
case DIR_Left: return DIR_Right;
|
|
|
|
case DIR_Right: return DIR_Left;
|
|
|
|
}
|
|
|
|
|
|
|
|
return AttackDir;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Sets the type of attack when StartFire is called */
|
|
|
|
simulated function InitAttackSequence(EPawnOctant NewAtkDir, EMeleeAttackType NewAtkType)
|
|
|
|
{
|
|
|
|
`log(Instigator@"SetNextAttackType:"@NewAtkDir, bLogMelee);
|
|
|
|
|
|
|
|
CurrentAtkChainIdx = 0;
|
|
|
|
NumChainedAttacks = 0;
|
|
|
|
NextAttackDir = NewAtkDir;
|
|
|
|
NextAttackType = NewAtkType;
|
|
|
|
|
|
|
|
// initialize chain attack sequence
|
|
|
|
if ( bUseDirectionalMelee )
|
|
|
|
{
|
|
|
|
AttackChainSequence.Length = 0;
|
|
|
|
|
|
|
|
if ( NewAtkType == ATK_DrawStrike )
|
|
|
|
{
|
|
|
|
AttackChainSequence = ChainSequence_R;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
switch (NextAttackDir)
|
|
|
|
{
|
|
|
|
case DIR_Forward: AttackChainSequence = ChainSequence_F; break;
|
|
|
|
case DIR_ForwardLeft: AttackChainSequence = ChainSequence_F; break;
|
|
|
|
case DIR_ForwardRight: AttackChainSequence = ChainSequence_F; break;
|
|
|
|
case DIR_Backward: AttackChainSequence = ChainSequence_B; break;
|
|
|
|
case DIR_Left: AttackChainSequence = ChainSequence_L; break;
|
|
|
|
case DIR_Right: AttackChainSequence = ChainSequence_R; break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bResetChainSequence = IsMeleeing();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the player uses a default melee attack within a short time of another attack, use combo anims
|
|
|
|
if ( NewAtkType == ATK_Normal && !IsMeleeing() )
|
|
|
|
{
|
|
|
|
if ( LastMeleeAnimEnd_ActorTime > 0.f && ActorTimeSince(LastMeleeAnimEnd_ActorTime) < 0.1f )
|
|
|
|
{
|
|
|
|
NextAttackType = ATK_Combo;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** see Weapon::RefireCheckTimer() */
|
|
|
|
simulated function MeleeCheckTimer()
|
|
|
|
{
|
|
|
|
ClearTimer(nameof(MeleeCheckTimer), self);
|
|
|
|
|
|
|
|
LastMeleeAnimEnd_ActorTime = GetActorTimeSeconds();
|
|
|
|
|
|
|
|
if ( IsMeleeing() )
|
|
|
|
{
|
|
|
|
// if switching to another weapon, abort firing and put down right away
|
|
|
|
if( bWeaponPutDown )
|
|
|
|
{
|
|
|
|
`LogInv("Weapon put down requested during fire, put it down now");
|
|
|
|
PutDownWeapon();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If weapon should keep on firing, then do not leave state and fire again.
|
|
|
|
if( bHasChainAttacks && ShouldContinueMelee(NumChainedAttacks) )
|
|
|
|
{
|
|
|
|
ContinueMeleeAttack();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise we're done firing
|
|
|
|
HandleFinishedFiring();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Called from state ShouldRefire */
|
|
|
|
simulated function ContinueMeleeAttack()
|
|
|
|
{
|
|
|
|
// increment attack sequence
|
|
|
|
if ( bUseDirectionalMelee && AttackChainSequence.Length > 0 )
|
|
|
|
{
|
|
|
|
NumChainedAttacks++;
|
|
|
|
|
|
|
|
// increment sequence unless it's been reset/interrupted
|
|
|
|
if ( !bResetChainSequence )
|
|
|
|
{
|
|
|
|
NextAttackDir = AttackChainSequence[CurrentAtkChainIdx];
|
|
|
|
CurrentAtkChainIdx = (CurrentAtkChainIdx + 1) % AttackChainSequence.Length;
|
|
|
|
}
|
|
|
|
|
|
|
|
`log("IncrementAttackSequence ChainIdx="$CurrentAtkChainIdx@"Direction="$NextAttackDir, bLogMelee);
|
|
|
|
}
|
|
|
|
|
|
|
|
// these attacks always play combo attack anims
|
|
|
|
NextAttackType = ATK_Combo;
|
|
|
|
|
|
|
|
BeginMeleeAttack(true);
|
|
|
|
|
|
|
|
// If this attack is our last attack in the sequence, clear pending fire.
|
|
|
|
if( PendingFire(CurrentFireMode) && !ShouldContinueMelee(NumChainedAttacks) )
|
|
|
|
{
|
|
|
|
ClearPendingFire(CurrentFireMode);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
* Damage & Hit FX
|
|
|
|
*********************************************************************************************/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see ProcessInstantHit
|
|
|
|
* Network: LocalPlayer and Server
|
|
|
|
*/
|
|
|
|
simulated function ProcessMeleeHit(byte FiringMode, ImpactInfo Impact)
|
|
|
|
{
|
|
|
|
local KActorFromStatic NewKActor;
|
|
|
|
local StaticMeshComponent HitStaticMesh;
|
|
|
|
local FracturedStaticMeshActor FracActor;
|
|
|
|
local KFPawn HitPawn;
|
|
|
|
local vector Momentum;
|
|
|
|
|
|
|
|
if ( Impact.HitActor != None )
|
|
|
|
{
|
|
|
|
if ( Impact.HitActor.bWorldGeometry )
|
|
|
|
{
|
|
|
|
HitStaticMesh = StaticMeshComponent(Impact.HitInfo.HitComponent);
|
|
|
|
if ( (HitStaticMesh != None) && HitStaticMesh.CanBecomeDynamic() )
|
|
|
|
{
|
|
|
|
NewKActor = class'KActorFromStatic'.Static.MakeDynamic(HitStaticMesh);
|
|
|
|
if ( NewKActor != None )
|
|
|
|
{
|
|
|
|
Impact.HitActor = NewKActor;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fracture meshes if we hit them
|
|
|
|
if( bAllowMeleeToFracture )
|
|
|
|
{
|
|
|
|
FracActor = FracturedStaticMeshActor(Impact.HitActor);
|
|
|
|
if(FracActor != None)
|
|
|
|
{
|
|
|
|
class'KFMeleeHelperBase'.static.MeleeFractureMeshImpact( FracActor, Impact.HitLocation, Impact.HitNormal );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Notify pawn of melee damage. Similar to NotifyTakeHit, but before AdjustDamage is called.
|
|
|
|
HitPawn = KFPawn(Impact.HitActor);
|
|
|
|
if ( HitPawn != None )
|
|
|
|
{
|
|
|
|
if( !bHitEnemyThisAttack && HitPawn.GetTeamNum() != Instigator.GetTeamNum() )
|
|
|
|
{
|
|
|
|
// Let the playercontroller know we did hit with this attack
|
|
|
|
if ( Instigator != None && Instigator.Controller != none &&
|
|
|
|
KFPlayerController(Instigator.Controller) != none )
|
|
|
|
{
|
|
|
|
KFPlayerController(Instigator.Controller).AddShotsHit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
bHitEnemyThisAttack = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
HitPawn.NotifyMeleeTakeHit(Instigator.Controller, Impact.HitLocation);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get momentum transfer from the owning weapon
|
|
|
|
Momentum = Normal(Impact.RayDir) * InstantHitMomentum[FiringMode];
|
|
|
|
|
|
|
|
// play effects before doing damage, because doing damage can change the actor (e.g. destructibles) and result in incorrect sounds
|
|
|
|
PlayMeleeHitEffects(Impact.HitActor, Impact.HitLocation, Impact.RayDir);
|
2020-12-13 15:09:05 +00:00
|
|
|
|
|
|
|
PawnTakeDamage(Impact, FiringMode, Momentum);
|
2020-12-13 15:01:13 +00:00
|
|
|
|
|
|
|
// notify weapon for custom code
|
|
|
|
NotifyMeleeCollision(Impact.HitActor, Impact.HitLocation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-13 15:09:05 +00:00
|
|
|
simulated function PawnTakeDamage(ImpactInfo Impact,byte FiringMode, vector Momentum)
|
|
|
|
{
|
|
|
|
Impact.HitActor.TakeDamage( GetMeleeDamage(FiringMode, Impact.RayDir), Instigator.Controller,
|
|
|
|
Impact.HitLocation, Momentum,
|
|
|
|
GetDamageType(FiringMode), Impact.HitInfo, Outer );
|
|
|
|
}
|
|
|
|
|
2020-12-13 15:01:13 +00:00
|
|
|
/** returns the damage type to use for this attack */
|
|
|
|
simulated function class<DamageType> GetDamageType(byte FiringMode)
|
|
|
|
{
|
|
|
|
// Use the FiringMode to determine type of attack
|
|
|
|
return InstantHitDamageTypes[FiringMode];
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Get damage scale adjusted to impact angle */
|
|
|
|
simulated function float GetDamageScaleByAngle(vector HitLoc)
|
|
|
|
{
|
|
|
|
local float DotResult;
|
|
|
|
local vector Origin;
|
|
|
|
local vector Aim;
|
|
|
|
|
|
|
|
if ( Instigator == None || !Instigator.IsLocallyControlled() )
|
|
|
|
{
|
|
|
|
return 1.f;
|
|
|
|
}
|
|
|
|
|
|
|
|
Origin = GetMeleeStartTraceLocation();
|
|
|
|
Aim = vector(Instigator.GetBaseAimRotation());
|
|
|
|
|
|
|
|
switch ( CurrentAttackDir )
|
|
|
|
{
|
|
|
|
case DIR_Left:
|
|
|
|
case DIR_Right:
|
|
|
|
// for horizontal attacks do 50-75% damage
|
|
|
|
DotResult = 0.75f * Normal2D(Aim) dot Normal2D(HitLoc - Origin);
|
|
|
|
return FMax(DotResult, 0.50f);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1.f;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called by ProcessMeleeHit to spawn effects
|
|
|
|
* Network: Local Player and Server
|
|
|
|
*/
|
|
|
|
simulated function PlayMeleeHitEffects(Actor Target, vector HitLocation, vector HitDirection, optional bool bShakeInstigatorCamera=true)
|
|
|
|
{
|
|
|
|
// @note: Skipping super(). No victim camera shake is intentional
|
|
|
|
|
|
|
|
if( WorldInfo.NetMode != NM_DedicatedServer )
|
|
|
|
{
|
|
|
|
// first person (local) fire effects
|
|
|
|
if ( Instigator.IsFirstPerson() )
|
|
|
|
{
|
|
|
|
PlayerController(Instigator.Controller).ClientPlayCameraShake(MeleeImpactCamShake, MeleeImpactCamShakeScale, true, CAPS_UserDefined, rotator(-HitDirection));
|
|
|
|
|
|
|
|
if ( Target.IsA('Pawn') )
|
|
|
|
{
|
|
|
|
AddBlood(0.01f, 0.1f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we hit a pawn we can skip the Tracing code in PlayImpactEffects. Pawn FX are handled in
|
|
|
|
// PlayHit and this prevents incorrect (world) FX from playing if the trace is bad.
|
|
|
|
if ( !(Target.bCanBeDamaged && Target.IsA('Pawn')) )
|
|
|
|
{
|
|
|
|
// Use ImpactEffectManager to material based world impacts
|
|
|
|
`ImpactEffectManager.PlayImpactEffects(HitLocation, Instigator, HitDirection, WorldImpactEffects);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
defaultproperties
|
|
|
|
{
|
|
|
|
bAllowMeleeToFracture=true
|
|
|
|
bHitboxPawnsOnly=true
|
|
|
|
|
|
|
|
// damage settings
|
|
|
|
InitialImpactDelay=0.2
|
|
|
|
ImpactRetryDuration=0.2
|
|
|
|
|
|
|
|
// default chain attack sequences
|
|
|
|
ChainSequence_F=(DIR_Left, DIR_ForwardRight, DIR_ForwardLeft, DIR_ForwardRight, DIR_ForwardLeft)
|
|
|
|
ChainSequence_B=(DIR_BackwardRight, DIR_ForwardLeft, DIR_BackwardLeft, DIR_ForwardRight, DIR_Left, DIR_Right, DIR_Left)
|
|
|
|
ChainSequence_L=(DIR_Right, DIR_Left, DIR_ForwardRight, DIR_ForwardLeft, DIR_Right, DIR_Left)
|
|
|
|
ChainSequence_R=(DIR_Left, DIR_Right, DIR_ForwardLeft, DIR_ForwardRight, DIR_Left, DIR_Right)
|
|
|
|
|
|
|
|
FatigueCurve=(Points=((InVal=2.f,OutVal=1.f),(InVal=15.f, OutVal=1.5f)))
|
|
|
|
|
|
|
|
WorldImpactEffects=KFImpactEffectInfo'FX_Impacts_ARCH.Blunted_melee_impact'
|
|
|
|
MeleeImpactCamShakeScale=1.f
|
|
|
|
}
|