//============================================================================= // KFWeaponAttachment //============================================================================= // Base class for third person weapon attachments //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // - Andrew "Strago" Ladenberger //============================================================================= class KFWeaponAttachment extends Actor hidecategories(Object, Movement, Attachment, Collision, Physics, Advanced, Debug, Mobile) native; /********************************************************************************************* * @name Mesh Components ********************************************************************************************* */ /** Editable SkeletalMesh set by the archetype */ var() const SkeletalMesh SkelMesh; /** Extra pawn anim set when weapon is equipped */ var() AnimSet CharacterAnimSet; /** Anim set to use for 3rd person weapon anims */ var() AnimSet WeaponAnimSet; /** Third Person Weapon SkelMesh */ var transient SkeletalMeshComponent WeapMesh; /** If true this third person weapon is part of the pawn's mesh */ var() bool bWeapMeshIsPawnMesh; /********************************************************************************************* * @name LaserSight ********************************************************************************************* */ var() bool bHasLaserSight; var() const KFLaserSightAttachment LaserSightArchetype; var transient KFLaserSightAttachment LaserSight; /********************************************************************************************* * @name Fire Effects ********************************************************************************************* */ /** A muzzle flash instance */ var KFMuzzleFlash MuzzleFlash; /** A reference to the muzzle flash template */ var() const KFMuzzleFlash MuzzleFlashTemplate; var float MaxFireEffectDistance; /** replicated information on a hit we've taken */ struct native KFTracerInfo { /** Tracer Effect */ var() ParticleSystem TracerTemplate; /** The velocity the tracer should travel at */ var() int TracerVelocity; /** Show the tracer when the weapon is firing in normal time */ var() bool bDoTracerDuringNormalTime; /** Show the tracer when the weapon is firing in zed time */ var() bool bDoTracerDuringZedTime; /** How far away the hitlocation has to be to spawn this tracer */ var int MinTracerEffectDistanceSquared; /** Actual tracer velocity vector, set at runtime */ var vector VelocityVector; structdefaultproperties { TracerVelocity=7000 bDoTracerDuringNormalTime=true bDoTracerDuringZedTime=false MinTracerEffectDistanceSquared=40000 } }; /** Tracer effect info per fire mode */ var() array TracerInfos; /********************************************************************************************* * @name Material Effects ********************************************************************************************* */ /** MIC for blood effects */ var transient MaterialInstanceConstant WeaponMIC; var transient float BloodParamValue; const BloodParamName = 'Scalar_Blood_Contrast'; const MinBloodParamValue = 0.20f; /********************************************************************************************* * @name Animation - (using const FNames for 'static' data) ********************************************************************************************* */ /** Replicated state of the 1st person weapon */ enum EWeaponState { WEP_Idle, WEP_Reload, WEP_ReloadEmpty, WEP_Reload_Elite, WEP_ReloadEmpty_Elite, WEP_ReloadSingle, WEP_ReloadSingleEmpty, WEP_ReloadSingle_Elite, WEP_ReloadSingleEmpty_Elite, WEP_ReloadSecondary, WEP_ReloadSecondary_Elite, WEP_ReloadSecondaryEmpty, WEP_ReloadSecondaryEmpty_Elite, WEP_ReloadDualsOneEmpty, WEP_ReloadDualsOneEmpty_Elite, WEP_MeleeBasic, WEP_MeleeChain, // @deprecated WEP_MeleeSustained, WEP_Melee_L, WEP_Melee_R, WEP_Melee_F, WEP_Melee_B, WEP_MeleeHeavy_L, WEP_MeleeHeavy_R, WEP_MeleeHeavy_F, WEP_MeleeHeavy_B, WEP_MeleeBlock, WEP_Cleaning, WEP_Equipping, WEP_PutAway, WEP_Grenade, WEP_Heal, WEP_HealQuick, WEP_Weld, WEP_Custom0, WEP_Custom1, WEP_Custom2, WEP_Custom3, WEP_Custom4, WEP_Custom5, WEP_Custom6, WEP_Custom7, WEP_Custom8, WEP_Custom9, }; /** Animation rate to scale all of our 3rd person animations by */ var protected transient float ThirdPersonAnimRate; /** Animation Physics Recoil */ var GameSkelCtrl_Recoil.RecoilDef Recoil_Hand; var GameSkelCtrl_Recoil.RecoilDef Recoil_Spine; /** MELEE */ const MeleeBasic = 'Melee'; const CH_MeleeBasic = 'Melee_CH'; /** directional attack anims */ const MeleeAnim_F = 'Atk_F'; const MeleeAnim_B = 'Atk_B'; const MeleeAnim_L = 'Atk_L'; const MeleeAnim_R = 'Atk_R'; const CH_MeleeAnim_F = 'Atk_F_CH'; const CH_MeleeAnim_B = 'Atk_B_CH'; const CH_MeleeAnim_L = 'Atk_L_CH'; const CH_MeleeAnim_R = 'Atk_R_CH'; /** heavy attack anims */ const MeleeHeavy_F = 'Atk_H_F'; const MeleeHeavy_B = 'Atk_H_B'; const MeleeHeavy_L = 'Atk_H_L'; const MeleeHeavy_R = 'Atk_H_R'; const CH_MeleeHeavy_F = 'Atk_H_F_CH'; const CH_MeleeHeavy_B = 'Atk_H_B_CH'; const CH_MeleeHeavy_L = 'Atk_H_L_CH'; const CH_MeleeHeavy_R = 'Atk_H_R_CH'; /** Continious (aka chainsaw) melee */ const MeleeLoopAnim = 'Atk_F_Loop'; const MeleeStartAnim = 'Atk_F_In'; const MeleeEndAnim = 'Atk_F_Out'; const CH_MeleeLoopAnim = 'Atk_F_Loop_CH'; const CH_MeleeStartAnim = 'Atk_F_In_CH'; const CH_MeleeEndAnim = 'Atk_F_Out_CH'; /** blocking/parry anims */ const BlockLoopAnim = 'Brace_Loop'; const BlockStartAnim = 'Brace_In'; const BlockEndAnim = 'Brace_Out'; const CH_BlockLoopAnim = 'Brace_Loop_CH'; const CH_BlockStartAnim = 'Brace_In_CH'; const CH_BlockEndAnim = 'Brace_Out_CH'; /** melee misc */ const CleanWeaponAnim = 'Clean_NoBlood'; const CH_CleanWeaponAnim = 'Clean_NoBlood_CH'; /** General, all inventory items anims */ const GrenadeAnim = 'Nade_Throw'; const EquipAnim = 'Equip'; const PutAwayAnim = 'PutAway'; const CH_GrenadeAnim = 'Nade_Throw_CH'; const CH_EquipAnim = 'Equip_CH'; const CH_PutAwayAnim = 'PutAway_CH'; /** Reload (magazine) anims */ const ReloadEmptyAnim = 'Reload_Empty'; const ReloadHalfAnim = 'Reload_Half'; const ReloadEmptyEliteAnim = 'Reload_Empty_Elite'; const ReloadHalfEliteAnim = 'Reload_Half_Elite'; const ReloadDualsOneEmptyAnim = 'Reload_Empty_Half'; const ReloadDualsOneEmptyEliteAnim = 'Reload_Empty_Half_Elite'; const CH_ReloadEmptyAnim = 'Reload_Empty_CH'; const CH_ReloadHalfAnim = 'Reload_Half_CH'; const CH_ReloadEmptyEliteAnim = 'Reload_Empty_Elite_CH'; const CH_ReloadHalfEliteAnim = 'Reload_Half_Elite_CH'; const CH_ReloadDualsOneEmptyAnim = 'Reload_Empty_Half_CH'; const CH_ReloadDualsOneEmptyEliteAnim = 'Reload_Empty_Half_Elite_CH'; /** Reload (single) anims */ const ReloadOpenAnim = 'Reload_Open'; const ReloadInsertAnim = 'Reload_Insert'; const ReloadCloseAnim = 'Reload_Close'; const ReloadOpenEliteAnim = 'Reload_Open_Elite'; const ReloadInsertEliteAnim = 'Reload_Insert_Elite'; const ReloadCloseEliteAnim = 'Reload_Close_Elite'; const ReloadOpenEmptyAnim = 'Reload_Open_Shell'; const ReloadOpenEmptyEliteAnim = 'Reload_Open_Shell_Elite'; const CH_ReloadOpenAnim = 'Reload_Open_CH'; const CH_ReloadInsertAnim = 'Reload_Insert_CH'; const CH_ReloadCloseAnim = 'Reload_Close_CH'; const CH_ReloadOpenEliteAnim = 'Reload_Open_Elite_CH'; const CH_ReloadInsertEliteAnim = 'Reload_Insert_Elite_CH'; const CH_ReloadCloseEliteAnim = 'Reload_Close_Elite_CH'; const CH_ReloadOpenEmptyAnim = 'Reload_Open_Shell_CH'; const CH_ReloadOpenEmptyEliteAnim = 'Reload_Open_Shell_Elite_CH'; /** Used only by the healing syringe */ const HealSelfAnim = 'Healer_Self'; const HealOtherAnim = 'Healer_F'; const QuickHealAnim = 'Heal_Quick'; const CH_HealSelfAnim = 'Healer_Self_CH'; const CH_HealOtherAnim = 'Healer_F_CH'; const CH_QuickHealAnim = 'Heal_Quick_CH'; /** Used only by the welder */ const WeldStartAnim = 'Welder_Start'; const WeldLoopAnim = 'Welder_Loop'; const WeldEndAnim = 'Welder_End'; const CH_WeldStartAnim = 'Welder_Start_CH'; const CH_WeldLoopAnim = 'Welder_Loop_CH'; const CH_WeldEndAnim = 'Welder_End_CH'; /** Additive shoot anims */ const ShootAnim = 'ADD_Shoot'; const CrouchShootAnim = 'ADD_CH_Shoot'; const IronShootAnim = 'ADD_Iron_Shoot'; /** Weapon shoots */ const WeaponFireAnim = 'Shoot'; const WeaponAltFireAnim = 'Shoot'; const WeaponIronFireAnim = 'Iron_Shoot'; /** (TEMP) blend settings */ var(Anims) float DefaultBlendInTime; var(Anims) float DefaultBlendOutTime; var(Anims) float ShootBlendInTime; var(Anims) float ShootBlendOutTime; /** Enable recoile skeletal controls */ var(Anims) bool bPlayIKRecoil; /** Save the last random index so we can choose a different one */ var transient byte LastMeleeAnimIdx; /** If set, check a character AnimNodeSlot and attempt to synchronize the weapon */ var transient bool bSynchronizeWeaponAnim; var transient AnimNodeSlot SyncPawnNode; var transient name SyncAnimName; var transient bool bSyncAnimCheckRelevance; /** Info for state LoopingWeaponAction */ var transient name LoopingAnim; var transient name LoopIntroAnim; var transient name LoopOutroAnim; var transient bool bLoopSynchedWeaponAnim; /** Cached AnimNodeSequence for the 3rd person weapon mesh */ var transient AnimNodeSequence WeapAnimNode; /** Profile name for this weapon */ var(Anims) name AimOffsetProfileName; /** Used by AimOffset node when bTurnOffWhenReloadingWeapon==TRUE */ var transient bool bIsReloading; var transient int WeaponSkinId; var bool bWaitingForWeaponSkinLoad; /********************************************************************************************* * @name Attach / Detach ********************************************************************************************* */ /** Weapon Mesh Attachment */ native event ChangeVisibility(bool bIsVisible); /********************************************************************************************* * @name Skins ********************************************************************************************* */ native function bool StartLoadWeaponSkin(int SkinId); event PreBeginPlay() { local int i; if ( WeapMesh != None && !bWeapMeshIsPawnMesh ) { if ( WeaponAnimSet != None ) { WeapMesh.AnimSets[0] = WeaponAnimSet; } if ( SkelMesh != none ) { // set the skeletal mesh from our archetype to the WeaponAttachment WeapMesh.SkeletalMesh = SkelMesh; } WeapAnimNode = AnimNodeSequence(WeapMesh.Animations); } for( i = 0; i < TracerInfos.Length; ++i ) { TracerInfos[i].VelocityVector = vect(1,0,0) * TracerInfos[i].TracerVelocity; } super.PreBeginPlay(); } /** Attach weapon to owner's skeletal mesh */ simulated function AttachTo(KFPawn P) { local byte WeaponAnimSetIdx; if ( bWeapMeshIsPawnMesh ) { WeapMesh = P.Mesh; } else if ( WeapMesh != None ) { // Attach Weapon mesh to player skel mesh WeapMesh.SetShadowParent(P.Mesh); P.Mesh.AttachComponent(WeapMesh, P.WeaponAttachmentSocket); } // Additional attachments if( bHasLaserSight && !P.IsFirstPerson() ) { AttachLaserSight(); } // Animation if ( CharacterAnimSet != None ) { WeaponAnimSetIdx = P.CharacterArch.GetWeaponAnimSetIdx(); P.Mesh.AnimSets[WeaponAnimSetIdx] = CharacterAnimSet; // update animations will reload all AnimSeqs with the new AnimSet P.Mesh.UpdateAnimations(); } // update aim offset nodes with new profile for this weapon P.SetAimOffsetNodesProfile(AimOffsetProfileName); //Do a first chance weapon skin switch (EX: changed weapon w/o ID changing by throwing a dualie) if (KFPawn_Human(P) != None && KFPawn_Human(P).WeaponSkinItemId > 0) { SetWeaponSkin(KFPawn_Human(P).WeaponSkinItemId); } } /** Detach weapon from owner's skeletal mesh */ simulated function DetachFrom(KFPawn P) { // detach effects if (MuzzleFlash != None) { MuzzleFlash.DetachMuzzleFlash(WeapMesh); } // Finally, detach weapon mesh if ( bWeapMeshIsPawnMesh ) { WeapMesh = None; } else if ( WeapMesh != None ) { WeapMesh.SetShadowParent(None); P.Mesh.DetachComponent( WeapMesh ); } } simulated function AttachMuzzleFlash() { if ( WeapMesh != none && MuzzleFlash == None ) { MuzzleFlash = new(self) Class'KFMuzzleFlash'(MuzzleFlashTemplate); MuzzleFlash.AttachMuzzleFlash(WeapMesh); } } simulated function AttachLaserSight() { if ( WeapMesh != none && LaserSight == None && LaserSightArchetype != None ) { LaserSight = new(self) Class'KFLaserSightAttachment' (LaserSightArchetype); LaserSight.AttachLaserSight(WeapMesh, false); } } /** * Assign weapon skin to 3rd person mesh */ event SetWeaponSkin(int ItemId, optional bool bFinishedLoading = false) { local array SkinMICs; if ( ItemId > 0 && WorldInfo.NetMode != NM_DedicatedServer && !bWaitingForWeaponSkinLoad) { if (!bFinishedLoading && StartLoadWeaponSkin(ItemId)) { return; } SkinMICs = class'KFWeaponSkinList'.static.GetWeaponSkin(ItemId, WST_ThirdPerson); if ( SkinMICs.Length > 0 ) { WeapMesh.SetMaterial(0, SkinMICs[0]); } } } /********************************************************************************************* * @name Fire Effect Methods ********************************************************************************************* */ /** * Need to either call Instigator.ActorEffectIsRelevant(), or determine our * own Location and LastRenderTime before calling Super.ActorEffectIsRelevant(). */ simulated function bool ActorEffectIsRelevant(Pawn EffectInstigator, bool bForceDedicated, optional float VisibleCullDistance=5000.0, optional float HiddenCullDistance=350.0 ) { if ( Instigator != None ) { return Instigator.ActorEffectIsRelevant(EffectInstigator, bForceDedicated, VisibleCullDistance, HiddenCullDistance); } return FALSE; } /** * The Weapon attachment, though hidden, is also responsible for controlling * the first person effects for a weapon. */ simulated function FirstPersonFireEffects(Weapon W, vector HitLocation) // Should be subclassed { if ( W != None ) { SpawnTracer(W.GetMuzzleLoc(), HitLocation); } } simulated function StopFirstPersonFireEffects(Weapon W) // Should be subclassed { if ( W != None ) { // Tell the weapon to cause the muzzle flash, etc. W.StopFireEffects( Pawn(Owner).FiringMode ); } } simulated function StartFire() { } simulated function StartPawnCrouch () { } simulated function EndPawnCrouch () { } simulated function SetWeaponUsingIronSights (bool bUsingIronSights) { } simulated function SetWeaponAltFireMode (bool bUsingAltFireMode) { } /** * Spawn all of the effects that will be seen in behindview/remote clients. This * function is called from the pawn, and should only be called when on a remote client or * if the local client is in a 3rd person mode. * @return TRUE if the effect culling check passes */ simulated function bool ThirdPersonFireEffects( vector HitLocation, KFPawn P, byte ThirdPersonAnimRateByte ) { local EAnimSlotStance AnimType; SpawnTracer(GetMuzzleLocation(), HitLocation); // Effects below this point are culled based on visibility and distance if ( !ActorEffectIsRelevant(P, false, MaxFireEffectDistance) ) { return false; } DecodeThirdPersonAnimRate( ThirdPersonAnimRateByte ); // Weapon shoot anims if( !bWeapMeshIsPawnMesh ) { PlayWeaponFireAnim(); } if( P.IsDoingSpecialMove() && P.SpecialMoves[P.SpecialMove].bAllowFireAnims ) { AnimType = EAS_Additive; } else { AnimType = EAS_FullBody; } // Character shoot anims if ( !P.IsDoingSpecialMove() || AnimType == EAS_Additive ) { PlayPawnFireAnim( P, AnimType ); // interrupt other weapon action anims (e.g. Reload) if( !P.IsDoingSpecialMove() ) { P.StopBodyAnim(P.bIsCrouched ? EAS_CH_UpperBody : EAS_UpperBody, 0.1f); } if ( OnWeaponStateChanged != None ) { OnWeaponStateChanged(true); } } CauseMuzzleFlash(P.FiringMode); return true; } /** Plays fire animation on weapon mesh */ simulated function PlayWeaponFireAnim() { local float Duration; if ( Instigator.bIsWalking ) { Duration = WeapMesh.GetAnimLength( WeaponIronFireAnim ); WeapMesh.PlayAnim( WeaponIronFireAnim, Duration / ThirdPersonAnimRate,, true ); } else { Duration = WeapMesh.GetAnimLength( WeaponFireAnim ); WeapMesh.PlayAnim( WeaponFireAnim, Duration / ThirdPersonAnimRate,, true ); } } /** Plays fire animation on pawn */ simulated function PlayPawnFireAnim( KFPawn P, EAnimSlotStance AnimType ) { if ( P.bIsCrouched ) { P.PlayBodyAnim(CrouchShootAnim, AnimType, ThirdPersonAnimRate, ShootBlendInTime, ShootBlendOutTime); } else if ( P.bIsWalking ) { P.PlayBodyAnim(IronShootAnim, AnimType, ThirdPersonAnimRate, ShootBlendInTime, ShootBlendOutTime); } else { P.PlayBodyAnim(ShootAnim, AnimType, ThirdPersonAnimRate, ShootBlendInTime, ShootBlendOutTime); } } simulated function StopThirdPersonFireEffects(optional bool bForce) { if (MuzzleFlash != None) { MuzzleFlash.StopMuzzleFlash(bForce); } } /** @return the starting location for effects (e.g. tracers) */ simulated function vector GetMuzzleLocation(optional byte MuzzleID) { local vector SocketLocation; if ( MuzzleFlashTemplate != none ) { WeapMesh.GetSocketWorldLocationAndRotation(MuzzleFlashTemplate.GetSocketName(), SocketLocation); return SocketLocation; } else { `log("Missing 3rd person muzzle socket for"@SkelMesh); return WeapMesh.Bounds.Origin + (vect(45,0,0) >> Instigator.Rotation); } } /** Spawn tracer effects for this weapon */ simulated function SpawnTracer(vector EffectLocation, vector HitLocation) { local ParticleSystemComponent E; local vector Dir; local float DistSQ; local float TracerDuration; local KFTracerInfo TracerInfo; if ( Instigator == None || Instigator.FiringMode >= TracerInfos.Length ) { return; } TracerInfo = TracerInfos[Instigator.FiringMode]; if( ((`NotInZedTime(self) && TracerInfo.bDoTracerDuringNormalTime) || (`IsInZedTime(self) && TracerInfo.bDoTracerDuringZedTime)) && TracerInfo.TracerTemplate != none ) { Dir = HitLocation - EffectLocation; DistSQ = VSizeSq(Dir); if ( DistSQ > TracerInfo.MinTracerEffectDistanceSquared ) { // Lifetime scales based on the distance from the impact point. Subtract a frame so it doesn't clip. TracerDuration = fMin( (Sqrt(DistSQ) - 100.f) / TracerInfo.TracerVelocity, 1.f ); if( TracerDuration > 0.f ) { E = WorldInfo.MyEmitterPool.SpawnEmitter( TracerInfo.TracerTemplate, EffectLocation, rotator(Dir) ); E.SetVectorParameter( 'Tracer_Velocity', TracerInfo.VelocityVector ); E.SetFloatParameter( 'Tracer_Lifetime', TracerDuration ); } } } } /** Attach if needed, trigger muzzle flash, trigger shell eject */ simulated function CauseMuzzleFlash(byte FiringMode) { if (MuzzleFlash == None && MuzzleFlashTemplate != None) { AttachMuzzleFlash(); } if (MuzzleFlash != None ) { MuzzleFlash.CauseMuzzleFlash(FiringMode); if ( MuzzleFlash.bAutoActivateShellEject ) { MuzzleFlash.CauseShellEject(); } } } /** notify to spawn a shell eject from the muzzle flash component */ simulated function ANIMNOTIFY_ShellEject() { if (MuzzleFlash == None && MuzzleFlashTemplate != None) { MuzzleFlash = new(self) Class'KFMuzzleFlash'(MuzzleFlashTemplate); MuzzleFlash.AttachMuzzleFlash(WeapMesh); } if (MuzzleFlash != None ) { MuzzleFlash.CauseShellEject(); } } /** Adds some value to this weapon's blood material parameter */ simulated function AddBattleBlood(float InBloodParamIncrementValue) { // Weapon shoot anims if( !bWeapMeshIsPawnMesh ) { if ( WeaponMIC == None && WeapMesh != None ) { WeaponMIC = WeapMesh.CreateAndSetMaterialInstanceConstant(0); } if ( WeaponMIC != None ) { BloodParamValue = FMax(BloodParamValue + InBloodParamIncrementValue, MinBloodParamValue); WeaponMIC.SetScalarParameterValue(BloodParamName, BloodParamValue); } } } /********************************************************************************************* * @name Character Animation ********************************************************************************************* */ delegate OnWeaponStateChanged(optional bool bInterrupted); /** * Plays a split (upper and lower body) animation on the owning pawn * Network: All but dedicated * * @param P Owning pawn to play animation on * @param AnimName Anim to play * @param bPlaySynchronizedWeaponAnim If true, try to play the same animation on the weapon mesh */ simulated function float PlayCharacterMeshAnim(KFPawn P, name AnimName, optional bool bPlaySynchedWeaponAnim, optional bool bLooping) { local float Duration; local EAnimSlotStance Stance; // skip weapon anims while in a special move if( P.IsDoingSpecialMove() && !P.SpecialMoves[P.SpecialMove].bAllowThirdPersonWeaponAnims ) { return 0.f; } Stance = (!P.bIsCrouched) ? EAS_UpperBody : EAS_CH_UpperBody; Duration = P.PlayBodyAnim(AnimName, Stance, ThirdPersonAnimRate, DefaultBlendInTime, DefaultBlendOutTime, bLooping); if ( Duration > 0 && bPlaySynchedWeaponAnim ) { PlayWeaponMeshAnim(AnimName, P.BodyStanceNodes[Stance], bLooping); } `log(GetFuncName()@"called on:"$P@"Anim:"$AnimName@"Duration:"$Duration, bDebug); return Duration; } function DecodeThirdPersonAnimRate( byte ThirdPersonAnimRateByte ) { ThirdPersonAnimRate = 1.f + ByteToFloat( ThirdPersonAnimRateByte ); } /** Called from the pawn when our first person weapon changes states */ simulated function UpdateThirdPersonWeaponAction(EWeaponState NewWeaponState, KFPawn P, byte ThirdPersonAnimRateByte ) { ClearTimer(nameof(LoopWeaponMeleeAnim)); bIsReloading = false; // We need our anim rate scale before doing anything DecodeThirdPersonAnimRate( ThirdPersonAnimRateByte ); if ( OnWeaponStateChanged != None ) { OnWeaponStateChanged(); } `log(GetFuncName()@"called on"@self@"State:"@EWeaponState(NewWeaponState), bDebug); switch (NewWeaponState) { case WEP_Equipping: PlayCharacterMeshAnim(P, P.bIsCrouched ? CH_EquipAnim : EquipAnim); break; case WEP_PutAway: PlayCharacterMeshAnim(P, P.bIsCrouched ? CH_PutAwayAnim : PutAwayAnim); break; case WEP_Grenade: PlayCharacterMeshAnim(P, P.bIsCrouched ? CH_GrenadeAnim : GrenadeAnim); break; case WEP_Heal: PlayHealAnim(P); break; case WEP_HealQuick: PlayCharacterMeshAnim(P, P.bIsCrouched ? CH_QuickHealAnim : QuickHealAnim); break; case WEP_Weld: PlayWeldAnim(P); break; case WEP_Cleaning: PlayCharacterMeshAnim(P, P.bIsCrouched ? CH_CleanWeaponAnim : CleanWeaponAnim); break; case WEP_MeleeBasic: //case WEP_MeleeChain: case WEP_Melee_B: case WEP_Melee_F: case WEP_Melee_L: case WEP_Melee_R: case WEP_MeleeHeavy_B: case WEP_MeleeHeavy_F: case WEP_MeleeHeavy_L: case WEP_MeleeHeavy_R: PlayMeleeAtkAnim(NewWeaponState, P); break; case WEP_MeleeSustained: PlayMeleeSustainedAnim(P); break; case WEP_MeleeBlock: PlayMeleeBlockAnim(P); break; case WEP_Reload: case WEP_ReloadEmpty: case WEP_Reload_Elite: case WEP_ReloadEmpty_Elite: case WEP_ReloadSecondary: case WEP_ReloadSecondary_Elite: case WEP_ReloadDualsOneEmpty: case WEP_ReloadDualsOneEmpty_Elite: case WEP_ReloadSecondaryEmpty: case WEP_ReloadSecondaryEmpty_Elite: bIsReloading = true; PlayReloadMagazineAnim(NewWeaponState, P); break; case WEP_ReloadSingle: case WEP_ReloadSingle_Elite: case WEP_ReloadSingleEmpty: case WEP_ReloadSingleEmpty_Elite: bIsReloading = true; PlayReloadSingleAnim(NewWeaponState, P); break; } } /** Play a melee attack animation */ simulated function float PlayMeleeAtkAnim(EWeaponState NewWeaponState, KFPawn P) { local name AnimName; local float Duration; // third-person melee notifies don't properly handle the bIgnoreIfActorHidden setting because their owning actor isn't ever hidden (because it's the pawn), // so just don't play third-person melee anims on first-person pawns (there's no need to anyway) if( P.IsFirstPerson() ) { return Duration; } switch (NewWeaponState) { case WEP_MeleeBasic: AnimName = P.bIsCrouched ? CH_MeleeBasic : MeleeBasic; break; case WEP_MeleeHeavy_B: AnimName = P.bIsCrouched ? CH_MeleeHeavy_B : MeleeHeavy_B; break; case WEP_MeleeHeavy_F: AnimName = P.bIsCrouched ? CH_MeleeHeavy_F : MeleeHeavy_F; break; case WEP_MeleeHeavy_L: AnimName = P.bIsCrouched ? CH_MeleeHeavy_L : MeleeHeavy_L; break; case WEP_MeleeHeavy_R: AnimName = P.bIsCrouched ? CH_MeleeHeavy_R : MeleeHeavy_R; break; case WEP_Melee_B: AnimName = P.bIsCrouched ? CH_MeleeAnim_B : MeleeAnim_B; break; case WEP_Melee_F: AnimName = P.bIsCrouched ? CH_MeleeAnim_F : MeleeAnim_F; break; case WEP_Melee_L: AnimName = P.bIsCrouched ? CH_MeleeAnim_L : MeleeAnim_L; break; case WEP_Melee_R: AnimName = P.bIsCrouched ? CH_MeleeAnim_R : MeleeAnim_R; break; //case WEP_MeleeChain: // AnimName = GetRandomDirectionalMeleeAnim(P); // break; } if ( AnimName != '' ) { Duration = PlayCharacterMeshAnim(P, AnimName); //if ( WeaponState == WEP_MeleeChain && Duration > 0 ) //{ // SetTimer(FMax(0.01, Duration - MeleeLoopAtkTimeFromEnd), false, nameof(LoopWeaponMeleeAnim)); //} } return Duration; } /** Helper for PlayMeleeAtkAnim */ simulated function name GetRandomDirectionalMeleeAnim(Pawn P) { local byte AnimIdx, NumTries; do { // pick a random animation, different than the last AnimIdx = Rand(4); NumTries++; } until ( LastMeleeAnimIdx != AnimIdx || NumTries >= 4 ); LastMeleeAnimIdx = AnimIdx; switch (AnimIdx) { case 0: return P.bIsCrouched ? CH_MeleeAnim_F : MeleeAnim_F; case 1: return P.bIsCrouched ? CH_MeleeAnim_B : MeleeAnim_B; case 2: return P.bIsCrouched ? CH_MeleeAnim_L : MeleeAnim_L; case 3: return P.bIsCrouched ? CH_MeleeAnim_R : MeleeAnim_R; } } /** Restart (aka Loop) this weapon state animation */ simulated function LoopWeaponMeleeAnim() { local KFPawn P; P = KFPawn(Owner); if ( P != None && !P.IsDoingSpecialMove() ) { UpdateThirdPersonWeaponAction( WEP_MeleeChain, P, P.GetWeaponAttachmentAnimRateByte() ); } } /** Play a 3rd person weapon action anim */ simulated function PlayMeleeBlockAnim(KFPawn P) { if ( P.bIsCrouched ) { StartLoopingAnim(P, CH_BlockLoopAnim, CH_BlockStartAnim, CH_BlockEndAnim); } else { StartLoopingAnim(P, BlockLoopAnim, BlockStartAnim, BlockEndAnim); } } /** Play a 3rd person weapon action anim */ simulated function PlayMeleeSustainedAnim(KFPawn P) { if ( P.bIsCrouched ) { StartLoopingAnim(P, CH_MeleeLoopAnim, CH_MeleeStartAnim, CH_MeleeEndAnim); } else { StartLoopingAnim(P, MeleeLoopAnim, MeleeStartAnim, MeleeEndAnim); } } /** Play a 3rd person reload animation */ simulated function PlayReloadMagazineAnim(EWeaponState NewWeaponState, KFPawn P) { local name AnimName; switch (NewWeaponState) { case WEP_Reload: AnimName = (!P.bIsCrouched) ? ReloadHalfAnim : CH_ReloadHalfAnim; break; case WEP_ReloadEmpty: AnimName = (!P.bIsCrouched) ? ReloadEmptyAnim : CH_ReloadEmptyAnim; break; case WEP_ReloadDualsOneEmpty: AnimName = (!P.bIsCrouched) ? ReloadDualsOneEmptyAnim : CH_ReloadDualsOneEmptyAnim; break; case WEP_Reload_Elite: AnimName = (!P.bIsCrouched) ? ReloadHalfEliteAnim : CH_ReloadHalfEliteAnim; break; case WEP_ReloadEmpty_Elite: AnimName = (!P.bIsCrouched) ? ReloadEmptyEliteAnim : CH_ReloadEmptyEliteAnim; break; case WEP_ReloadDualsOneEmpty_Elite: AnimName = (!P.bIsCrouched) ? ReloadDualsOneEmptyEliteAnim : CH_ReloadDualsOneEmptyEliteAnim; break; } PlayCharacterMeshAnim(P, AnimName, true); } /** Play a 3rd person reload animation */ simulated function PlayReloadSingleAnim(EWeaponState NewWeaponState, KFPawn P) { switch (NewWeaponState) { case WEP_ReloadSingle: if ( P.bIsCrouched ) StartLoopingAnim(P, CH_ReloadInsertAnim, CH_ReloadOpenAnim, CH_ReloadCloseAnim, true); else StartLoopingAnim(P, ReloadInsertAnim, ReloadOpenAnim, ReloadCloseAnim, true); break; case WEP_ReloadSingle_Elite: if ( P.bIsCrouched ) StartLoopingAnim(P, CH_ReloadInsertEliteAnim, CH_ReloadOpenEliteAnim, CH_ReloadCloseEliteAnim, true); else StartLoopingAnim(P, ReloadInsertEliteAnim, ReloadOpenEliteAnim, ReloadCloseEliteAnim, true); break; case WEP_ReloadSingleEmpty: if ( P.bIsCrouched ) StartLoopingAnim(P, CH_ReloadInsertAnim, CH_ReloadOpenEmptyAnim, CH_ReloadCloseAnim, true); else StartLoopingAnim(P, ReloadInsertAnim, ReloadOpenEmptyAnim, ReloadCloseAnim, true); break; case WEP_ReloadSingleEmpty_Elite: if ( P.bIsCrouched ) StartLoopingAnim(P, CH_ReloadInsertEliteAnim, CH_ReloadOpenEmptyEliteAnim, CH_ReloadCloseEliteAnim, true); else StartLoopingAnim(P, ReloadInsertEliteAnim, ReloadOpenEmptyEliteAnim, ReloadCloseEliteAnim, true); break; } } /** Play a 3rd person heal animation */ simulated function PlayHealAnim(KFPawn P) { if ( P.bIsCrouched ) { PlayCharacterMeshAnim(P, (P.FiringMode == 0) ? CH_HealOtherAnim : CH_HealSelfAnim); } else { PlayCharacterMeshAnim(P, (P.FiringMode == 0) ? HealOtherAnim : HealSelfAnim); } } /* Plays a 3rd person weld animation */ simulated function PlayWeldAnim(KFPawn P) { if ( P.bIsCrouched ) { StartLoopingAnim(P, CH_WeldLoopAnim, CH_WeldStartAnim, CH_WeldEndAnim); } else { StartLoopingAnim(P, WeldLoopAnim, WeldStartAnim, WeldEndAnim); } } /********************************************************************************************* * @name Weapon Animation ********************************************************************************************* */ simulated function PlayWeaponMeshAnim(name AnimName, AnimNodeSlot SyncNode, bool bLoop) { local float Duration; // Weapon shoot anims if( !bWeapMeshIsPawnMesh ) { Duration = WeapMesh.GetAnimLength(AnimName); WeapMesh.PlayAnim(AnimName, Duration / ThirdPersonAnimRate, bLoop); // syncronize this with the character anim if ( SyncNode != None ) { bSynchronizeWeaponAnim = true; SyncPawnNode = SyncNode; SyncAnimName = AnimName; bSyncAnimCheckRelevance = false; } } } /** Tick is used by bSynchronizeWeaponAnim */ simulated event Tick( float DeltaTime ) { // If we're playing a a syncronzied weapon action, check the owner pawn's animation if ( !bWeapMeshIsPawnMesh && bSynchronizeWeaponAnim && SyncPawnNode != None && WeapMesh.bForceRefpose == 0 ) { // check to see if the character anim is still playing if ( !SyncPawnNode.bIsPlayingCustomAnim || (!SyncPawnNode.bRelevant && bSyncAnimCheckRelevance) || SyncPawnNode.GetPlayedAnimation() != SyncAnimName ) { if ( WeapAnimNode.bPlaying && WeapAnimNode.AnimSeqName == SyncAnimName ) { InterruptWeaponAnim(); } bSynchronizeWeaponAnim = false; } // After character mesh has been ticked once start checking node relevance bSyncAnimCheckRelevance = true; } } /** Stops a currently playing 3rd person weapon animation */ simulated function InterruptWeaponAnim() { // Weapon shoot anims if( !bWeapMeshIsPawnMesh ) { WeapAnimNode.StopAnim(); // Return to RefPos, because StopAnim doesn't call OnAnimEnd if ( WeapAnimNode.bForceRefposeWhenNotPlaying ) { WeapMesh.SetForceRefPose(TRUE); } } } /********************************************************************************************* * @name Rendering/Lighting ********************************************************************************************* */ /** Set the lighting channels on all the appropriate weapon attachment mesh(es) */ simulated function SetMeshLightingChannels(LightingChannelContainer NewLightingChannels) { if( !bWeapMeshIsPawnMesh ) { WeapMesh.SetLightingChannels(NewLightingChannels); } if( LaserSight != none ) { LaserSight.SetMeshLightingChannels(NewLightingChannels); } } /** Debug */ simulated function bool HasIndoorLighting() { return WeapMesh.LightingChannels.Indoor; } /** Debug */ simulated function bool HasOutdoorLighting() { return WeapMesh.LightingChannels.Outdoor; } /********************************************************************************************* * State LoopingWeaponAction * Plays a looping character animation with optional intro/outro anims. Most weapon * actions don't use state logic, but looping is complex enough that states are useful. *********************************************************************************************/ simulated function StartLoopingAnim(KFPawn P, name InLoopAnim, optional name InIntroAnim, optional name InOutroAnim, optional bool bPlaySynchedWeaponAnim) { if ( !P.IsDoingSpecialMove() ) { LoopingAnim = InLoopAnim; LoopIntroAnim = InIntroAnim; LoopOutroAnim = InOutroAnim; bLoopSynchedWeaponAnim = bPlaySynchedWeaponAnim; GotoState('LoopingWeaponAction'); } } // Global declarations for LoopingWeaponAction simulated function PlayLoopAnim(); simulated State LoopingWeaponAction { /** Play intro anim and start timer */ simulated function BeginState(name PreviousStateName) { local KFPawn P; local float Duration; P = KFPawn(Owner); if ( LoopIntroAnim != '' ) { Duration = PlayCharacterMeshAnim(P, LoopIntroAnim, bLoopSynchedWeaponAnim); if ( Duration > 0 ) { // 0.2f should match the blend in time of the loop anim SetTimer(Duration - 0.2f, false, nameof(PlayLoopAnim)); return; } } // no intro, start immediately PlayLoopAnim(); } /** Make sure looping anim is stopped */ simulated function EndState(Name NextStateName) { local KFPawn P; P = KFPawn(Owner); if ( P != None ) { P.StopBodyAnim(EAS_UpperBody, 0.1f); P.StopBodyAnim(EAS_CH_UpperBody, 0.1f); } if ( bLoopSynchedWeaponAnim && bSynchronizeWeaponAnim ) { InterruptWeaponAnim(); } } /** Play main looping anim */ simulated function PlayLoopAnim() { local KFPawn P; P = KFPawn(Owner); if ( P != None ) { PlayCharacterMeshAnim(P, LoopingAnim, bLoopSynchedWeaponAnim, true); } } /** Stop looping state and play outro anim */ simulated function UpdateThirdPersonWeaponAction( EWeaponState NewWeaponState, KFPawn P, byte ThirdPersonAnimRateByte ) { GotoState(''); if( LoopOutroAnim != '' ) { DecodeThirdPersonAnimRate( ThirdPersonAnimRateByte ); PlayCharacterMeshAnim(P, LoopOutroAnim, bLoopSynchedWeaponAnim); } Global.UpdateThirdPersonWeaponAction( NewWeaponState, P, ThirdPersonAnimRateByte ); } /** Stop looping state */ simulated function bool ThirdPersonFireEffects( vector HitLocation, KFPawn P, byte ThirdPersonAnimRateByte ) { GotoState(''); return Global.ThirdPersonFireEffects( HitLocation, P, ThirdPersonAnimRateByte ); } /** Stop looping state */ simulated function DetachFrom(KFPawn P) { GotoState(''); Global.DetachFrom(P); } } /** Special event added for weap attachments. Free for use */ function OnSpecialEvent(int Arg); defaultproperties { Begin Object class=AnimNodeSequence Name=MeshSequenceA bForceRefposeWhenNotPlaying=true End Object // Weapon SkeletalMesh Begin Object Class=SkeletalMeshComponent Name=SkeletalMeshComponent0 bOwnerNoSee=true bOnlyOwnerSee=false CollideActors=false AlwaysLoadOnClient=true AlwaysLoadOnServer=true MaxDrawDistance=4000 bUpdateSkelWhenNotRendered=false bIgnoreControllersWhenNotRendered=true bOverrideAttachmentOwnerVisibility=true bAcceptsDynamicDecals=FALSE Animations=MeshSequenceA CastShadow=true bCastDynamicShadow=true bPerBoneMotionBlur=true bForceRefPose=1 // Default to outdoor. If indoor, this will be set when TWIndoorLightingVolume::Touch() event is received at spawn. LightingChannels=(Outdoor=TRUE,bInitialized=TRUE) End Object WeapMesh=SkeletalMeshComponent0 LaserSightArchetype=KFLaserSightAttachment'FX_LaserSight_ARCH.Default_LaserSight_3P' TickGroup=TG_DuringAsyncWork NetUpdateFrequency=10 RemoteRole=ROLE_None bReplicateInstigator=true MaxFireEffectDistance=5000.0 AimOffsetProfileName=Default // temp DefaultBlendInTime=0.2f DefaultBlendOutTime=0.2f ShootBlendInTime=0.1 ShootBlendOutTime=0.1 ThirdPersonAnimRate=1.0f LastMeleeAnimIdx=255 bWaitingForWeaponSkinLoad=false }