//============================================================================= // 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 */ var private EPawnOctant NextAttackDir; var private EMeleeAttackType NextAttackType; /** 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 ChainSequence_F; var() array ChainSequence_B; var() array ChainSequence_L; var() array ChainSequence_R; /** Cached chain attack sequence currently in use */ var array 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 *********************************************************************************************/ simulated function EPawnOctant GetNextAttackDir() { return NextAttackDir; } simulated function SetNextAttackDir(EPawnOctant _nextAttackDir) { NextAttackDir = _nextAttackDir; } /** 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 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 ImpactList, optional vector Extent) { local KFPawn KFPOwner; local KFSM_InteractionPawnFollower FollowerSM; local Pawn BestVictim; local ImpactInfo CurrentImpact; local array 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 VictimList, vector StartTrace, vector EndTrace, vector RayDir, optional out array 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 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 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); PawnTakeDamage(Impact, FiringMode, Momentum); // notify weapon for custom code NotifyMeleeCollision(Impact.HitActor, Impact.HitLocation); } } 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 ); } /** returns the damage type to use for this attack */ simulated function class 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 }