//============================================================================= // KFWeap_MeleeBase //============================================================================= // Base class used for weapons where melee is the primary firemode //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // - Andrew "Strago" Ladenberger //============================================================================= class KFWeap_MeleeBase extends KFWeapon abstract native; /************************************************************************************ * @name Firing / Timing / States ***********************************************************************************/ /** These override the base firemodes of the same ID (for readability) */ const BLOCK_FIREMODE = 1; // ALTFIRE_FIREMODE const HEAVY_ATK_FIREMODE = 5; // NEW - IronSights Key /** Set when blood material value is high */ var bool bIsBloody; /** Maximum number of attacks that can be played without releasing fire */ var byte MaxChainAtkCount; /** Minimum amount of time to go to the MeleeSustained state for */ var float MinMeleeSustainedTime; /** Minimum amount of time to wait before dealing damage in the MeleeSustained state */ var() float MeleeSustainedWarmupTime; /** Amount of time required between cancelling attacks with reload*/ var private const float ReloadCancelTimeLimit; /** Whether this can be interrupted by another attack/reload/etc. */ var bool StartFireDisabled; /** Special flag in order to make weapons like the frost shotgun axe (frostfang) have the access to perk skills */ var bool bHasToBeConsideredAsRangedWeaponForPerks; /********************************************************************************************* * @name Defensive Abilities *********************************************************************************************/ struct native BlockEffectInfo { var class DmgType; /** If != None, overrides the class default FX */ var AkEvent BlockSound; var AkEvent ParrySound; var ParticleSystem BlockParticleSys; var ParticleSystem ParryParticleSys; }; var array BlockTypes; /** Damage while blocking will be mitigated by this percentage */ var() float BlockDamageMitigation; /** Parry damage will be mitigated by this percentage */ var() float ParryDamageMitigationPercent; /** Hit reaction strength to bypass pawn's ParryStumbleResist */ var() byte ParryStrength; /** If true, owning pawn moves at a slower (iron sight) walking speed */ var bool bMoveAtWalkingSpeed; /** Time between block hit reaction anims */ var() protected float BlockHitAnimCooldownTime; /** The last time we played a block hit reaction anim */ var transient protected float LastBlockHitAnimTime; /********************************************************************************************* * @name Animation *********************************************************************************************/ /** 4-way directional melee attack animation names */ const MeleeAttackAnim_F = 'Atk_F'; const MeleeAttackAnim_B = 'Atk_B'; const MeleeAttackAnim_L = 'Atk_L'; const MeleeAttackAnim_R = 'Atk_R'; /** 4-way directional heavy attack animation names */ const MeleeHeavyAttackAnim_F = 'Atk_H_F'; const MeleeHeavyAttackAnim_B = 'Atk_H_B'; const MeleeHeavyAttackAnim_L = 'Atk_H_L'; const MeleeHeavyAttackAnim_R = 'Atk_H_R'; /** 8-way directional combo chain animation names */ const MeleeComboChainAnim_F = 'Combo_F'; const MeleeComboChainAnim_FL = 'Combo_FL'; const MeleeComboChainAnim_FR = 'Combo_FR'; const MeleeComboChainAnim_B = 'Combo_B'; const MeleeComboChainAnim_BL = 'Combo_BL'; const MeleeComboChainAnim_BR = 'Combo_BR'; const MeleeComboChainAnim_L = 'Combo_L'; const MeleeComboChainAnim_R = 'Combo_R'; /** Special ability attacks */ const MeleeDrawStrikeAnim = 'Atk_Draw'; /** Defensive stance animation names */ const MeleeBlockStartAnim = 'Brace_in'; const MeleeBlockLoopAnim = 'Brace_loop'; const MeleeBlockEndAnim = 'Brace_out'; /** Weapon wear/cleaning */ const CleanBloodyAnim = 'Clean_Blood'; const CleanNonBloodyAnim = 'Clean_NoBlood'; /** MeleeSustained default anims - To modify this in a subclass override GetLoopingFireAnim, etc... */ const MeleeSustainedLoopAnim = 'Atk_F_Loop'; const MeleeSustainedStartAnim = 'Atk_F_In'; const MeleeSUstainedEndAnim = 'Atk_F_Out'; /** Settle animations played after an attack */ var array MeleeAttackSettleAnims; /** Animations played on successful block */ var array MeleeBlockHitAnims; /********************************************************************************************* * @name Effects ********************************************************************************************* */ /** (deprecated) particle system templates for trail notifies */ var ParticleSystem DistortTrailParticle; var ParticleSystem WhiteTrailParticle; var ParticleSystem BlueTrailParticle; var ParticleSystem RedTrailParticle; /** Block / Parry */ var AkBaseSoundObject BlockSound; var AKBaseSoundObject ParrySound; var ParticleSystem BlockParticleSystem; var ParticleSystem ParryParticleSystem; var name BlockEffectsSocketName; /********************************************************************************************* * @name Trader ********************************************************************************************* */ /** Estimated attack rate for this weapon, taking chaining, etc. into account. * Basically, just eyeball the animations and take an average. */ var() byte EstimatedFireRate; cpptext { // custom trail overrides virtual class UParticleSystem* GetAnimTrailParticleSystem(const class UAnimNotify_Trails* AnimNotifyData) const; } /********************************************************************************************* * @name Instigator Movement Speed ********************************************************************************************* */ /** If TRUE, Instigator/Owner moves at walking speed when this weapon is equipped */ simulated function bool ShouldOwnerWalk() { return bMoveAtWalkingSpeed; } /** Used to toggle bMoveAtWalkingSpeed */ simulated function SetSlowMovement(bool bEnabled) { if ( Instigator.IsLocallyControlled() ) { bMoveAtWalkingSpeed = bEnabled; if ( Role < ROLE_Authority ) { ServerSetSlowMovement(bEnabled); } } } /** Called by local player to synchronize movement the same way ServerZoomIn/ServerZoomOut does */ reliable server function ServerSetSlowMovement(bool bEnabled) { bMoveAtWalkingSpeed = bEnabled; } /********************************************************************************************* * @name Firing / Projectile ********************************************************************************************* */ /** * @see Weapon::StartFire */ simulated function StartFire(byte FireModeNum) { // can't start fire because it's in an uninterruptible state if (StartFireDisabled) { return; } // if the weapon is currently attacking if (CurrentFireMode == DEFAULT_FIREMODE || CurrentFireMode == ALTFIRE_FIREMODE || CurrentFireMode == BASH_FIREMODE) { // and the player tries to cancel with a reload action if(FireModeNum == RELOAD_FIREMODE) { // stop them from reload cancelling if it has already happened too recently if (IsTimerActive(nameof(Timer_FireCancel))) { return; } else { SetTimer(ReloadCancelTimeLimit, false, nameof(Timer_FireCancel)); } } } // Get attack type and send to server if( FireModeNum == DEFAULT_FIREMODE || FireModeNum == HEAVY_ATK_FIREMODE ) { StartMeleeFire(FireModeNum, MeleeAttackHelper.ChooseAttackDir(), ATK_Normal); return; } // If we're going to pretend bash is primary (see SendToFiringState) need to init directional melee if( FireModeNum == BASH_FIREMODE && WeaponFireTypes[FireModeNum] == EWFT_None ) { StartMeleeFire(FireModeNum, MeleeAttackHelper.ChooseAttackDir(), ATK_Normal); return; } Super.StartFire(FireModeNum); } simulated function Timer_FireCancel() {} /** * Like StartFire, but replicates a attack type * Network: LocalPlayer */ simulated function StartMeleeFire(byte FireModeNum, EPawnOctant AttackDir, EMeleeAttackType AtkType) { MeleeAttackHelper.InitAttackSequence(AttackDir, AtkType); // derived from StartFire() if( Instigator == None || !Instigator.bNoWeaponFiring ) { if( Role < Role_Authority ) { // if we're a client, synchronize server ServerStartMeleeFire(FireModeNum, AttackDir, AtkType); } // Start fire locally BeginFire(FireModeNum); } } /** * Sets the desired attack type for an incoming melee fire mode * Network: Dedicated Server only, or Listen Server for remote clients. */ reliable server private function ServerStartMeleeFire(byte FireModeNum, EPawnOctant AttackDir, EMeleeAttackType AtkType) { MeleeAttackHelper.InitAttackSequence(AttackDir, AtkType); Super.ServerStartFire(FireModeNum); } /** Instead of switch fire mode use as immediate alt fire */ simulated function AltFireMode() { if ( !Instigator.IsLocallyControlled() ) { return; } // StartFire - StopFire called from KFPlayerInput StartFire(BLOCK_FIREMODE); } /** Route ironsight player input to heavy attack */ simulated function SetIronSights(bool bNewIronSights) { if ( !Instigator.IsLocallyControlled() ) { return; } if ( bNewIronSights ) { StartFire(HEAVY_ATK_FIREMODE); } else { StopFire(HEAVY_ATK_FIREMODE); } } /** * Send weapon to proper firing state */ simulated function SendToFiringState(byte FireModeNum) { // If we don't have anything assigned to BASH_FIREMODE (V) route to primary attack (LMB) if( FireModeNum == BASH_FIREMODE && WeaponFireTypes[FireModeNum] == EWFT_None ) { Super.SendToFiringState(DEFAULT_FIREMODE); ClearPendingFire(BASH_FIREMODE); } Super.SendToFiringState(FireModeNum); } /********************************************************************************************* * @name Damage ********************************************************************************************* */ /** process local player impact for clientside hit detection */ event RecieveClientImpact(byte FiringMode, const out ImpactInfo Impact, optional out float PenetrationValue, optional int ImpactNum) { MeleeAttackHelper.ProcessMeleeHit(FiringMode, Impact); } /** returns the damage amount for this attack */ simulated function int GetMeleeDamage(byte FireModeNum, optional vector RayDir) { local int Damage; local KFPerk InstigatorPerk; Damage = GetModifiedDamage(FireModeNum, RayDir); // decode damage scale (see GetDamageScaleByAngle) from the RayDir if ( !IsZero(RayDir) ) { Damage = Round(float(Damage) * FMin(VSize(RayDir), 1.f)); } InstigatorPerk = GetPerk(); if( InstigatorPerk != none ) { if( IsHeavyAttack( FireModeNum ) ) { InstigatorPerk.ModifyHardAttackDamage( Damage ); } else if( IsLightAttack(FireModeNum) ) { InstigatorPerk.ModifyLightAttackDamage( Damage ); } } return Damage; } /********************************************************************************************* * @name Blood/Wear Effects *********************************************************************************************/ /** Increment bloody material parameter --- Network: Local Player */ simulated function AddBlood(float MinAmount, float MaxAmount) { Super.AddBlood(MinAmount, MaxAmount); if ( !bIsBloody ) { bIsBloody = true; ServerSetBloody(true); } } /** Syncronize this with the server for reloading state */ reliable server private function ServerSetBloody(bool bNewIsBloody) { bIsBloody = bNewIsBloody; } /** Remove blood material parameter from this weapon */ simulated function ANIMNOTIFY_CleanBlood() { local int i; bIsBloody = false; if ( WorldInfo.NetMode != NM_DedicatedServer && WeaponMICs.Length > 0 ) { BloodParamValue = 0.f; for( i = 0; i < WeaponMICs.Length; ++i ) { if( WeaponMICs[i] != none ) { WeaponMICs[i].SetScalarParameterValue( BloodParamName, BloodParamValue ); } } } } /********************************************************************************************* * @name Animation ********************************************************************************************* */ /** Overriden to use WeaponAnimNodeSeq */ simulated function PlayWeaponAnimation(name Sequence, float fDesiredDuration, optional bool bLoop, optional SkeletalMeshComponent SkelMesh) { Super.PlayWeaponAnimation(Sequence, fDesiredDuration, bLoop, SkelMesh); // Reset CameraAnim following bFollowAnimSeqCamera = default.bFollowAnimSeqCamera; } /** Called from the MeleeHelper class to allow for the weapon to override settings */ simulated function PlayMeleeAnimation(name AnimName, out float out_Rate, float BlendTime) { Super.PlayMeleeAnimation(AnimName, out_Rate, BlendTime); // Enable CameraAnim following bFollowAnimSeqCamera = true; `DialogManager.PlayMeleeAttackDialog( KFPawn(Instigator), IsHeavyAttack(CurrentFireMode) ); } /********************************************************************************************* * State WeaponUpkeep * A firemode that replaces reload where the user cleans or sharpens the weapon *********************************************************************************************/ simulated function UpkeepComplete(); /** Returns true if weapon can potentially be reloaded */ simulated function bool CanReload(optional byte FireModeNum) { if ( FiringStatesArray[RELOAD_FIREMODE] == 'WeaponUpkeep' ) { return true; } return Super.CanReload(FireModeNum); } simulated state WeaponUpkeep { simulated function byte GetWeaponStateId() { return WEP_Cleaning; } simulated function BeginState(name PreviousStateName) { local name AnimName; local float Duration; // Calc Reload Duration AnimName = (bIsBloody) ? CleanBloodyAnim : CleanNonBloodyAnim; Duration = MySkelMesh.GetAnimInterruptTime(AnimName); if ( Duration > 0.f ) { if ( Instigator.IsFirstPerson() ) { PlayAnimation(AnimName); } SetTimer(Duration, FALSE, nameof(UpkeepComplete)); } else { `warn("Duration is zero!!!"@AnimName); SetTimer(0.001, FALSE, nameof(UpkeepComplete)); } NotifyBeginState(); } // Allow for players to skip cleaning the weapon and go right into attacking. simulated function BeginFire(byte FireModeNum) { Global.BeginFire(FireModeNum); // handle reload interrupts if ( FireModeNum != RELOAD_FIREMODE ) { // if able, immediately interupt/abort the reload state if( PendingFire(FireModeNum) && HasAmmo(FireModeNum) ) { ClearPendingFire(RELOAD_FIREMODE); GotoState('Active'); } } } simulated event EndState(Name NextStateName) { NotifyEndState(); } /** Cleanup state */ simulated function UpkeepComplete() { // we're done, leave state and go back to active GotoState('Active'); } } simulated function bool CanOverrideMagReload(byte FireModeNum) { return Super.CanOverrideMagReload(FireModeNum) || FireModeNum == GRENADE_FIREMODE; } /********************************************************************************************* * State MeleeChainAttacking * A melee firemode that chains together a sequence of attacks *********************************************************************************************/ simulated function bool IsLightAttack( byte FireMode ); simulated state MeleeChainAttacking extends MeleeAttackBasic { simulated function BeginState(Name PrevStateName) { super.BeginState( PrevStateName ); } simulated function byte GetWeaponStateId() { switch (MeleeAttackHelper.CurrentAttackDir) { case DIR_Forward: return WEP_Melee_F; case DIR_ForwardLeft: return WEP_Melee_F; case DIR_ForwardRight: return WEP_Melee_F; case DIR_Backward: return WEP_Melee_B; case DIR_BackwardLeft: return WEP_Melee_B; case DIR_BackwardRight: return WEP_Melee_B; case DIR_Left: return WEP_Melee_L; case DIR_Right: return WEP_Melee_R; } return WEP_MeleeChain; } /** Called when primary fire is let go, or when ShouldContinueMelee returns false */ simulated function EndState(Name NextStateName) { Super.EndState(NextStateName); PlayMeleeSettleAnim(); // If both melee attacks are pressed, give priority to heavy attack. Need // to do this because BeginFire is called in numeric order. if ( PendingFire(DEFAULT_FIREMODE) && PendingFire(HEAVY_ATK_FIREMODE) ) { ClearPendingFire(DEFAULT_FIREMODE); } } /** Checks to see if we can perform another melee strike without changing states */ simulated function bool ShouldContinueMelee(optional int ChainCount) { // If next attack is of a different fire mode we want leave the attacking state so // that the FiringState and FiringMode are properly updated for the new attack. if ( PendingFire(HEAVY_ATK_FIREMODE) ) { return false; } // normal weapon path (pending fire, etc...) if ( !ShouldRefire() ) { return false; } // make sure our firemode handler supports chain attacks if ( !MeleeAttackHelper.bHasChainAttacks ) { return false; } return (ChainCount + 1) < MaxChainAtkCount; } /** Get name of the animation to play for PlayFireEffects */ simulated function name GetMeleeAnimName(EPawnOctant AtkDir, EMeleeAttackType AtkType) { // Update our animation rate before changing weapon state to stay synced UpdateWeaponAttachmentAnimRate( GetThirdPersonAnimRate() ); // update state id to match new attack direction KFPawn(Instigator).WeaponStateChanged(GetWeaponStateId()); // primary / normal strikes and chain attacks if ( AtkType == ATK_Combo ) { switch (AtkDir) { case DIR_Forward: return MeleeComboChainAnim_F; case DIR_ForwardLeft: return MeleeComboChainAnim_FL; case DIR_ForwardRight: return MeleeComboChainAnim_FR; case DIR_Backward: return FRand() < 0.5f ? MeleeComboChainAnim_BL : MeleeComboChainAnim_BR; case DIR_BackwardLeft: return MeleeComboChainAnim_BL; case DIR_BackwardRight: return MeleeComboChainAnim_BR; case DIR_Left: return MeleeComboChainAnim_L; case DIR_Right: return MeleeComboChainAnim_R; } } switch (AtkDir) { case DIR_Forward: return MeleeAttackAnim_F; case DIR_ForwardLeft: return MeleeAttackAnim_F; case DIR_ForwardRight: return MeleeAttackAnim_F; case DIR_Backward: return MeleeAttackAnim_B; case DIR_BackwardLeft: return MeleeAttackAnim_B; case DIR_BackwardRight: return MeleeAttackAnim_B; case DIR_Left: return MeleeAttackAnim_L; case DIR_Right: return MeleeAttackAnim_R; } } simulated function bool IsLightAttack( byte FireMode ) { return true; } /** Gets the current animation rate, scaled or not */ simulated function float GetThirdPersonAnimRate() { local float ScaledRate; ScaledRate = EvalInterpCurveFloat( MeleeAttackHelper.FatigueCurve, MeleeAttackHelper.NumChainedAttacks ); ModifyMeleeAttackSpeed(ScaledRate, CurrentFireMode); return 1.f / ScaledRate; } } /** Plays a 'settle' animation after a melee attack is finished */ simulated function PlayMeleeSettleAnim() { local int AnimIdx; if( MeleeAttackSettleAnims.Length > 0 ) { AnimIdx = Rand(MeleeAttackSettleAnims.Length); PlayAnimation(MeleeAttackSettleAnims[AnimIdx], 0.0, false, 0.1); } } /********************************************************************************************* * State MeleeHeavyAttacking * This is the alt-fire Melee State. *********************************************************************************************/ simulated function bool IsHeavyAttack(byte FireMode); simulated state MeleeHeavyAttacking extends MeleeAttackBasic { simulated function byte GetWeaponStateId() { switch (MeleeAttackHelper.CurrentAttackDir) { case DIR_Forward: return WEP_MeleeHeavy_F; case DIR_ForwardLeft: return WEP_MeleeHeavy_F; case DIR_ForwardRight: return WEP_MeleeHeavy_F; case DIR_Backward: return WEP_MeleeHeavy_B; case DIR_BackwardLeft: return WEP_MeleeHeavy_B; case DIR_BackwardRight: return WEP_MeleeHeavy_B; case DIR_Left: return WEP_MeleeHeavy_L; case DIR_Right: return WEP_MeleeHeavy_R; } return WEP_Idle; } simulated function BeginState(name PreviousStateName) { Super.BeginState(PreviousStateName); // freeze the player for a short time for heavy damage strikes if ( Instigator.IsLocallyControlled() ) { KFPlayerController(Instigator.Controller).PauseMoveInput(0.1f); } } simulated function EndState(Name NextStateName) { Super.EndState(NextStateName); PlayMeleeSettleAnim(); } /** heavy damage attack anims */ simulated function name GetMeleeAnimName(EPawnOctant AtkDir, EMeleeAttackType AtkType) { // heavy damage attacks if ( AtkType == ATK_DrawStrike ) { return MeleeDrawStrikeAnim; } switch (AtkDir) { case DIR_Forward: return MeleeHeavyAttackAnim_F; case DIR_ForwardLeft: return MeleeHeavyAttackAnim_F; case DIR_ForwardRight: return MeleeHeavyAttackAnim_F; case DIR_Backward: return MeleeHeavyAttackAnim_B; case DIR_BackwardLeft: return MeleeHeavyAttackAnim_B; case DIR_BackwardRight: return MeleeHeavyAttackAnim_B; case DIR_Left: return MeleeHeavyAttackAnim_L; case DIR_Right: return MeleeHeavyAttackAnim_R; } } simulated function bool IsHeavyAttack(byte FireMode) { return true; } } /********************************************************************************************* * State MeleeSustained * A special melee state used for chainsaw like weapons. Extends WeaponFiring which is a * bit odd, but it shares more in common with it than it does with MeleeAttackBasic *********************************************************************************************/ // Global declarations for this state function SustainedMinFireTimer(); function SustainedWarmupEndTimer(); simulated state MeleeSustained extends WeaponFiring { ignores AllowSprinting, AllowIronSights; simulated function bool IsMeleeing() { return TRUE; } simulated function BeginState(Name PreviousStateName) { // @note: No super because we don't want to FireAmmunition() right away local KFPerk InstigatorPerk; InstigatorPerk = GetPerk(); if( InstigatorPerk != none ) { SetZedTimeResist( InstigatorPerk.GetZedTimeModifier(self) ); } if ( MeleeSustainedWarmupTime > 0.f ) { SetTimer(MeleeSustainedWarmupTime, false, nameof(SustainedWarmupEndTimer)); } else { SustainedWarmupEndTimer(); } StartLoopingFireEffects(CurrentFireMode, true); NotifyBeginState(); } simulated function EndState(Name NextStateName) { Super.EndState(NextStateName); NotifyEndState(); } /** Handle MinMeleeSustainedTime */ simulated function bool StillFiring(byte FireMode) { if ( Global.StillFiring(FireMode) || IsTimerActive(nameof(SustainedMinFireTimer)) ) { return true; } return false; } /** Called after warmup when weapon is ready to fire */ simulated function SustainedWarmupEndTimer() { // Do the first damage right away, we've already waited for the warmup time FireAmmunition(); TimeWeaponFiring(CurrentFireMode); if ( MinMeleeSustainedTime > 0.f ) { SetTimer(MinMeleeSustainedTime, false, nameof(SustainedMinFireTimer)); } } /** Performs melee hit detection and does damage */ simulated function FireAmmunition() { HandleWeaponShotTaken( CurrentFireMode ); MeleeAttackHelper.bHitEnemyThisAttack = false; MeleeAttackHelper.MeleeAttackImpact(); // Use ammunition to fire ConsumeAmmo( CurrentFireMode ); } /** Get name of the animation to play for PlayFireEffects */ simulated function name GetLoopingFireAnim(byte FireModeNum) { return MeleeSustainedLoopAnim; } /** Get name of the animation to play for PlayFireEffects */ simulated function name GetLoopStartFireAnim(byte FireModeNum) { return MeleeSustainedStartAnim; } /** Get name of the animation to play for PlayFireEffects */ simulated function name GetLoopEndFireAnim(byte FireModeNum) { return MeleeSustainedEndAnim; } simulated function byte GetWeaponStateId() { return WEP_MeleeSustained; } } /********************************************************************************************* * State MeleeBlocking * This is the default Blocking State. It's performed on both the client and the server. *********************************************************************************************/ // Global declarations for blocking state simulated function BlockLoopTimer(); simulated function ParryCheckTimer(); /** Called on the server when successfully block/parry an attack */ unreliable client function ClientPlayBlockEffects(optional byte BlockTypeIndex=255) { local AkBaseSoundObject Sound; local ParticleSystem PSTemplate; GetBlockEffects(BlockTypeIndex, Sound, PSTemplate); PlayLocalBlockEffects(Sound, PSTemplate); } /** Called on the server when successfully block/parry an attack */ reliable client function ClientPlayParryEffects(optional byte BlockTypeIndex=255) { local AkBaseSoundObject Sound; local ParticleSystem PSTemplate; local KFPerk InstigatorPerk; InstigatorPerk = GetPerk(); if( InstigatorPerk != none ) { InstigatorPerk.SetSuccessfullParry(); } GetParryEffects(BlockTypeIndex, Sound, PSTemplate); PlayLocalBlockEffects(Sound, PSTemplate); } simulated function NotifyAttackParried(); simulated function NotifyAttackBlocked(); simulated state MeleeBlocking { ignores ForceReload, ShouldAutoReload; simulated function byte GetWeaponStateId() { return WEP_MeleeBlock; } simulated function BeginState(name PreviousStateName) { local float ParryDuration; ParryDuration = PlayBlockStart(); // Set the duration of the window to parry incoming attacks if ( ParryDuration > 0.f ) { SetTimer( ParryDuration, false, nameof(ParryCheckTimer) ); } NotifyBeginState(); } simulated function EndState(Name NextStateName) { if ( Instigator.IsLocallyControlled() ) { PlayAnimation(MeleeBlockEndAnim); } //SetSlowMovement(false); NotifyEndState(); } /** Return to active state if we're done blocking */ simulated function EndFire(byte FireModeNum) { Global.EndFire(FireModeNum); // Wait until parry is finished, then check PendingFire to stop blocking if ( !StillFiring(CurrentFireMode) && !IsTimerActive(nameof(ParryCheckTimer)) ) { GotoState('BlockingCooldown'); } } /** After the parry window is finished, check PendingFire to see if we're still blocking */ simulated function ParryCheckTimer() { // Check PendingFire to stop blocking if ( !StillFiring(CurrentFireMode) ) { GotoState('BlockingCooldown'); } } /** Grab/Grapple attacks can be parried */ function bool IsGrappleBlocked(Pawn InstigatedBy) { local float FacingDot; local vector Dir2d; // zero Z to give us a 2d dot product Dir2d = Normal2d(InstigatedBy.Location - Instigator.Location); FacingDot = vector(Instigator.Rotation) dot (Dir2d); // Cos(85) if ( FacingDot > 0.087f ) { if ( IsTimerActive(nameof(ParryCheckTimer)) ) { KFPawn(InstigatedBy).NotifyAttackParried(Instigator, 255); ClientPlayParryEffects(); NotifyAttackParried(); } else { ClientPlayBlockEffects(); NotifyAttackBlocked(); } return TRUE; } return FALSE; } /** While holding a melee weapon reduce some incoming damage */ function AdjustDamage(out int InDamage, class DamageType, Actor DamageCauser) { local float FacingDot; local vector Dir2d; local KFPerk InstigatorPerk; local byte BlockTypeIndex; // don't apply block/parry effects for teammates if (Instigator.IsSameTeam(DamageCauser.Instigator)) { return; } // zero Z to give us a 2d dot product Dir2d = Normal2d(DamageCauser.Location - Instigator.Location); FacingDot = vector(Instigator.Rotation) dot (Dir2d); // Cos(85) if ( FacingDot > 0.087f && CanBlockDamageType(DamageType, BlockTypeIndex) ) { InstigatorPerk = GetPerk(); if ( IsTimerActive(nameof(ParryCheckTimer)) ) { InDamage *= GetUpgradedParryDamageMitigation(CurrentWeaponUpgradeIndex); // Notify attacking pawn for effects / animations if ( KFPawn(DamageCauser) != None ) { KFPawn(DamageCauser).NotifyAttackParried(Instigator, ParryStrength); } // @NOTE: This is now always true per discussion with AndrewL on KFII-29686. Since we always // do the damage mitigation, we should always play the effect regardless of whether the // zed was stumbled or knocked down. -MattF ClientPlayParryEffects(BlockTypeIndex); NotifyAttackParried(); if( InstigatorPerk != none ) { InstigatorPerk.SetSuccessfullParry(); } } else { InDamage *= GetUpgradedBlockDamageMitigation(CurrentWeaponUpgradeIndex); ClientPlayBlockEffects(BlockTypeIndex); NotifyAttackBlocked(); if( InstigatorPerk != none ) { InstigatorPerk.SetSuccessfullBlock(); } } } } simulated function BlockLoopTimer() { if( Instigator.IsLocallyControlled() ) { PlayAnimation(MeleeBlockLoopAnim, , true); } } /** State override for Block_Hit animations */ unreliable client function ClientPlayBlockEffects(optional byte BlockTypeIndex=255) { local int AnimIdx; local float Duration; local KFPerk InstigatorPerk; Global.ClientPlayBlockEffects(BlockTypeIndex); InstigatorPerk = GetPerk(); if( InstigatorPerk != none ) { InstigatorPerk.SetSuccessfullBlock(); } if( MeleeBlockHitAnims.Length > 0 && `TimeSince(LastBlockHitAnimTime) > BlockHitAnimCooldownTime && !IsTimerActive(nameof(ParryCheckTimer)) ) { AnimIdx = Rand(MeleeBlockHitAnims.Length); Duration = MySkelMesh.GetAnimLength(MeleeBlockHitAnims[AnimIdx]); if ( Duration > 0 ) { LastBlockHitAnimTime = WorldInfo.TimeSeconds; PlayAnimation(MeleeBlockHitAnims[AnimIdx]); SetTimer(Duration, false, nameof(BlockLoopTimer)); } } } } simulated function float PlayBlockStart() { local float AnimDuration; if( Instigator.IsLocallyControlled() ) { PlayAnimation(MeleeBlockStartAnim); } // set when to start playing the looping anim AnimDuration = MySkelMesh.GetAnimLength(MeleeBlockStartAnim); if ( AnimDuration > 0.f ) { SetTimer(AnimDuration, false, nameof(BlockLoopTimer)); } else { BlockLoopTimer(); } // set the parry duration to the same as the block start anim return AnimDuration; } /** Called on the client when successfully block/parry an attack */ simulated function PlayLocalBlockEffects(AKBaseSoundObject Sound, ParticleSystem PSTemplate) { local vector Loc; local rotator Rot; local ParticleSystemComponent PSC; if ( Sound != None ) { PlaySoundBase(Sound, true); } if ( PSTemplate != None ) { if ( MySkelMesh.GetSocketWorldLocationAndRotation(BlockEffectsSocketName, Loc, Rot) ) { PSC = WorldInfo.MyEmitterPool.SpawnEmitter(PSTemplate, Loc, Rot); PSC.SetDepthPriorityGroup(SDPG_Foreground); } else { `log(self@GetFuncName()@"missing BlockEffects Socket!"); } } } /** If true, this damage type can be blocked by the MeleeBlocking state */ function bool CanBlockDamageType(class DamageType, optional out byte out_Idx) { local int Idx; // Check if this damage should be ignored completely for (Idx = 0; Idx < BlockTypes.length; ++Idx) { if ( ClassIsChildOf(DamageType, BlockTypes[Idx].DmgType) ) { out_Idx = Idx; return true; } } out_Idx = INDEX_NONE; return false; } /** Returns sound and particle system overrides using index into BlockTypes array */ simulated function GetBlockEffects(byte BlockIndex, out AKBaseSoundObject outSound, out ParticleSystem outParticleSys) { outSound = BlockSound; outParticleSys = BlockParticleSystem; if ( BlockIndex != 255 ) { if ( BlockTypes[BlockIndex].BlockSound != None ) { outSound = BlockTypes[BlockIndex].BlockSound; } if ( BlockTypes[BlockIndex].BlockParticleSys != None ) { outParticleSys = BlockTypes[BlockIndex].BlockParticleSys; } } } /** Returns sound and particle system overrides using index into BlockTypes array */ simulated function GetParryEffects(byte BlockIndex, out AKBaseSoundObject outSound, out ParticleSystem outParticleSys) { outSound = ParrySound; outParticleSys = ParryParticleSystem; if ( BlockIndex != 255 ) { if ( BlockTypes[BlockIndex].ParrySound != None ) { outSound = BlockTypes[BlockIndex].ParrySound; } if ( BlockTypes[BlockIndex].ParryParticleSys != None ) { outParticleSys = BlockTypes[BlockIndex].ParryParticleSys; } } } /********************************************************************************************* * State BlockingCooldown * A short cooldown state to prevent spamming block while still allowing pendingfire to be set *********************************************************************************************/ // Global declarations for this state simulated function BlockCooldownTimer(); simulated state BlockingCooldown extends Active { ignores AllowSprinting; /** Set cooldown duration */ simulated function BeginState( Name PreviousStateName ) { SetTimer(0.5, false, nameof(BlockCooldownTimer)); Super.BeginState(PreviousStateName); } // prevent going to block/parry state simulated function bool HasAmmo( byte FireModeNum, optional int Amount ) { if ( FireModeNum == BLOCK_FIREMODE ) { return false; } return Global.HasAmmo(FireModeNum, Amount); } // prevent HasAmmo (above) from causing an auto reload simulated function bool ShouldAutoReload(byte FireModeNum) { if ( FireModeNum == BLOCK_FIREMODE ) { return false; } return Global.ShouldAutoReload(FireModeNum); } simulated function BlockCooldownTimer() { GotoState('Active'); } } /********************************************************************************************* * State WeaponEquipping * Overridden for DrawStrike ability *********************************************************************************************/ /** Perform a special attack animation while equipping */ simulated function ANIMNOTIFY_DrawAtk(); simulated function AttemptDrawStrike(); simulated state WeaponEquipping { /** Override BeginFire so that it will enter the firing state right away. */ simulated function ANIMNOTIFY_DrawAtk() { // wait a short time because we can't call SetAnim during a notify SetTimer(0.001f, false, nameof(AttemptDrawStrike)); } /** If melee attack fire modes are pending, perform draw strike */ simulated function AttemptDrawStrike() { // if either of the fire modes are pending, perform them if ( PendingFire(DEFAULT_FIREMODE) || PendingFire(HEAVY_ATK_FIREMODE) ) { MeleeAttackHelper.InitAttackSequence(DIR_Right, ATK_DrawStrike); SendToFiringState(HEAVY_ATK_FIREMODE); } } } /********************************************************************************************* * Trader ********************************************************************************************/ /** Allows weapon to set its own trader stats (can set number of stats, names and values of stats) */ static simulated event SetTraderWeaponStats( out array WeaponStats ) { super.SetTraderWeaponStats( WeaponStats ); WeaponStats.Length = WeaponStats.Length + 1; WeaponStats[WeaponStats.Length-1].StatType = TWS_Block; WeaponStats[WeaponStats.Length-1].StatValue = GetUpgradedBlockDamageMitigation(0); WeaponStats.Length = WeaponStats.Length + 1; WeaponStats[WeaponStats.Length-1].StatType = TWS_Parry; WeaponStats[WeaponStats.Length-1].StatValue = GetUpgradedParryDamageMitigation(0); } /** Allows weapon to calculate its own damage for display in trader * In general, melee weapons should use heavy attack to determine damage */ static simulated function float CalculateTraderWeaponStatDamage() { local float CalculatedDamage; local class DamageType; CalculatedDamage = default.InstantHitDamage[HEAVY_ATK_FIREMODE]; DamageType = class(default.InstantHitDamageTypes[HEAVY_ATK_FIREMODE]); if( DamageType != none && DamageType.default.DoT_Type != DOT_None ) { CalculatedDamage += (DamageType.default.DoT_Duration / DamageType.default.DoT_Interval) * (CalculatedDamage * DamageType.default.DoT_DamageScale); } return CalculatedDamage; } /** Allows weapon to calculate its own fire rate for display in trader. * Overridden to use estimated fire rate. */ static simulated function float CalculateTraderWeaponStatFireRate() { // attack interval in anim-based, so there's no way to compute it programatically from the editor, where // this function is called from. return default.EstimatedFireRate; } /** Returns trader filter index based on weapon type */ static simulated event EFilterTypeUI GetTraderFilter() { return FT_Melee; } /********************************************************************************************* * Upgrades ********************************************************************************************/ static simulated function float GetUpgradedBlockDamageMitigation(int UpgradeIndex) { return GetUpgradedStatValue(default.BlockDamageMitigation, EWUS_BlockDmgMitigation, UpgradeIndex); } static simulated function float GetUpgradedParryDamageMitigation(int UpgradeIndex) { return GetUpgradedStatValue(default.ParryDamageMitigationPercent, EWUS_ParryDmgMitigation, UpgradeIndex); } defaultproperties { InventoryGroup=IG_Melee bMeleeWeapon=true Begin Object Name=FirstPersonMesh // Set MinTickTimeStep to ensure a high enough tickrate for melee hitbox collision MinTickTimeStep=0.025f // 40fps End Object // DEFAULT_FIREMODE FireModeIconPaths(DEFAULT_FIREMODE)=Texture2D'ui_firemodes_tex.UI_FireModeSelect_Melee' FiringStatesArray(DEFAULT_FIREMODE)=MeleeChainAttacking WeaponFireTypes(DEFAULT_FIREMODE)=EWFT_Custom InstantHitDamageTypes(DEFAULT_FIREMODE)=class'KFDT_Slashing' InstantHitDamage(DEFAULT_FIREMODE)=20 AmmoCost(DEFAULT_FIREMODE)=0 // ALT_FIREMODE FireModeIconPaths(HEAVY_ATK_FIREMODE)=Texture2D'ui_firemodes_tex.UI_FireModeSelect_Melee' FiringStatesArray(HEAVY_ATK_FIREMODE)=MeleeHeavyAttacking WeaponFireTypes(HEAVY_ATK_FIREMODE)=EWFT_Custom InstantHitDamageTypes(HEAVY_ATK_FIREMODE)=class'KFDT_Slashing' InstantHitDamage(HEAVY_ATK_FIREMODE)=50 InstantHitMomentum(HEAVY_ATK_FIREMODE)=1.f AmmoCost(HEAVY_ATK_FIREMODE)=0 // RELOAD_FIREMODE FiringStatesArray(RELOAD_FIREMODE)=WeaponUpkeep // MELEE_BLOCK_FIREMODE FiringStatesArray(BLOCK_FIREMODE)=MeleeBlocking WeaponFireTypes(BLOCK_FIREMODE)=EWFT_Custom FireInterval(BLOCK_FIREMODE)=1.f AmmoCost(BLOCK_FIREMODE)=0 Begin Object Name=MeleeHelper_0 bUseDirectionalMelee=true bHasChainAttacks=true bUseMeleeHitTimer=false MaxHitRange=150 End Object // default MIC param names BlockEffectsSocketName=BlockEffect // Attachments bHasIronSights=false bHasFlashlight=false // Aim Assist AimCorrectionSize=0.f bTargetAdhesionEnabled=false //-------------------------------- // Defensive BlockDamageMitigation=0.50f ParryDamageMitigationPercent=0.2 ParryStrength=4 BlockHitAnimCooldownTime=0.5f BlockTypes.Add((DmgType=class'KFDT_Bludgeon')) BlockTypes.Add((DmgType=class'KFDT_Slashing')) BlockSound=none ParrySound=none BlockParticleSystem=ParticleSystem'FX_Impacts_EMIT.FX_Block_melee_01' ParryParticleSystem=ParticleSystem'FX_Impacts_EMIT.FX_Parry_melee_01' MeleeBlockHitAnims=(Block_Hit_V1, Block_Hit_V2, Block_Hit_V3); DistortTrailParticle = ParticleSystem'FX_Gameplay_EMIT_THREE.Trails.FX_Trail_Distort_R_01' WhiteTrailParticle = ParticleSystem'FX_Gameplay_EMIT_THREE.Trails.FX_Trail_White_R_01' BlueTrailParticle = ParticleSystem'FX_Gameplay_EMIT_THREE.Trails.FX_Trail_Blue_R_01' RedTrailParticle = ParticleSystem'FX_Gameplay_EMIT_THREE.Trails.FX_Trail_Red_R_01' //-------------------------------- // Animations MaxChainAtkCount = 3 MeleeAttackSettleAnims.Add(Settle_V1) MinMeleeSustainedTime = 0.5f MeleeSustainedWarmupTime = 0.25f // Trader EstimatedFireRate = 100 // Upgrades UpgradeFireModes(BLOCK_FIREMODE) = 0 UpgradeFireModes(HEAVY_ATK_FIREMODE) = 1 UpgradeFireModes(CUSTOM_FIREMODE) = 1 ReloadCancelTimeLimit = 0.5f; bHasToBeConsideredAsRangedWeaponForPerks=false; }