//============================================================================= // KFWeap_RocketLauncher_Seeker6 //============================================================================= // A heatseeking rocket launcher that can fire up to 6 rockets //============================================================================= // Killing Floor 2 // Copyright (C) 2017 Tripwire Interactive LLC // - Matt 'Squirrlz' Farber //============================================================================= class KFWeap_RocketLauncher_Seeker6 extends KFWeap_GrenadeLauncher_Base; const MAX_LOCKED_TARGETS = 6; /** Constains all currently locked-on targets */ var protected array LockedTargets; /** How much to scale recoil when firing in multi-rocket mode */ var float BurstFireRecoilModifier; /** The last time a target was acquired */ var protected float LastTargetLockTime; /** The last time a target validation check was performed */ var protected float LastTargetValidationCheckTime; /** How much time after a lock on occurs before another is allowed */ var const float TimeBetweenLockOns; /** How much time should pass between target validation checks */ var const float TargetValidationCheckInterval; /** Minimum distance a target can be from the crosshair to be considered for lock on */ var const float MinTargetDistFromCrosshairSQ; /** Dot product FOV that targets need to stay within to maintain a target lock */ var const float MaxLockMaintainFOVDotThreshold; /** Sound Effects to play when Locking */ var AkBaseSoundObject LockAcquiredSoundFirstPerson; var AkBaseSoundObject LockLostSoundFirstPerson; /** Icon textures for lock on drawing */ var const Texture2D LockedOnIcon; var LinearColor LockedIconColor; /** Ironsights Audio */ var AkComponent IronsightsComponent; var AkEvent IronsightsZoomInSound; var AkEvent IronsightsZoomOutSound; /** * Toggle between DEFAULT and ALTFIRE */ simulated function AltFireMode() { super.AltFireMode(); LockedTargets.Length = 0; } /********************************************************************************************* * @name Target Locking And Validation **********************************************************************************************/ /** We need to update our locked targets every frame and make sure they're within view and not dead */ simulated event Tick( float DeltaTime ) { local Pawn RecentlyLocked, StaticLockedTargets[6]; local bool bUpdateServerTargets; local int i; super.Tick( DeltaTime ); if( bUsingSights && bUseAltFireMode && Instigator != none && Instigator.IsLocallyControlled() ) { if( `TimeSince(LastTargetLockTime) > TimeBetweenLockOns && LockedTargets.Length < AmmoCount[GetAmmoType(0)] && LockedTargets.Length < MAX_LOCKED_TARGETS) { bUpdateServerTargets = FindTargets( RecentlyLocked ); } if( LockedTargets.Length > 0 ) { bUpdateServerTargets = bUpdateServerTargets || ValidateTargets( RecentlyLocked ); } // If we are a client, synchronize our targets with the server if( bUpdateServerTargets && Role < ROLE_Authority ) { for( i = 0; i < MAX_LOCKED_TARGETS; ++i ) { if( i < LockedTargets.Length ) { StaticLockedTargets[i] = LockedTargets[i]; } else { StaticLockedTargets[i] = none; } } ServerSyncLockedTargets( StaticLockedTargets ); } } } /** * Given an potential target TA determine if we can lock on to it. By default only allow locking on * to pawns. */ simulated function bool CanLockOnTo(Actor TA) { Local KFPawn PawnTarget; PawnTarget = KFPawn(TA); // Make sure the pawn is legit, isn't dead, and isn't already at full health if ((TA == None) || !TA.bProjTarget || TA.bDeleteMe || (PawnTarget == None) || (TA == Instigator) || (PawnTarget.Health <= 0) || !HasAmmo(DEFAULT_FIREMODE)) { return false; } // Make sure and only lock onto players on the same team return !WorldInfo.GRI.OnSameTeam(Instigator, TA); } /** Finds a new lock on target, adds it to the target array and returns TRUE if the array was updated */ simulated function bool FindTargets( out Pawn RecentlyLocked ) { local Pawn P, BestTargetLock; local byte TeamNum; local vector AimStart, AimDir, TargetLoc, Projection, DirToPawn, LinePoint; local Actor HitActor; local float PointDistSQ, Score, BestScore; TeamNum = Instigator.GetTeamNum(); AimStart = GetSafeStartTraceLocation(); AimDir = vector( GetAdjustedAim(AimStart) ); BestScore = 0.f; //Don't add targets if we're already burst firing if (IsInState('WeaponBurstFiring')) { return false; } foreach WorldInfo.AllPawns( class'Pawn', P ) { if (!CanLockOnTo(P)) { continue; } // Want alive pawns and ones we already don't have locked if( P != none && P.IsAliveAndWell() && P.GetTeamNum() != TeamNum && LockedTargets.Find(P) == INDEX_NONE ) { TargetLoc = GetLockedTargetLoc( P ); Projection = TargetLoc - AimStart; DirToPawn = Normal( Projection ); // Filter out pawns too far from center if( AimDir dot DirToPawn < 0.5f ) { continue; } // Check to make sure target isn't too far from center PointDistToLine( TargetLoc, AimDir, AimStart, LinePoint ); PointDistSQ = VSizeSQ( LinePoint - P.Location ); if( PointDistSQ > MinTargetDistFromCrosshairSQ ) { continue; } // Make sure it's not obstructed HitActor = class'KFAIController'.static.ActorBlockTest(self, TargetLoc, AimStart,, true, true); if( HitActor != none && HitActor != P ) { continue; } // Distance from target has much more impact on target selection score Score = VSizeSQ( Projection ) + PointDistSQ; if( BestScore == 0.f || Score < BestScore ) { BestTargetLock = P; BestScore = Score; } } } if( BestTargetLock != none ) { LastTargetLockTime = WorldInfo.TimeSeconds; LockedTargets.AddItem( BestTargetLock ); RecentlyLocked = BestTargetLock; // Plays sound/FX when locking on to a new target PlayTargetLockOnEffects(); return true; } RecentlyLocked = none; return false; } /** Checks to ensure all of our current locked targets are valid */ simulated function bool ValidateTargets( optional Pawn RecentlyLocked ) { local int i; local bool bShouldRemoveTarget, bAlteredTargets; local vector AimStart, AimDir, TargetLoc; local Actor HitActor; if( `TimeSince(LastTargetValidationCheckTime) < TargetValidationCheckInterval ) { return false; } LastTargetValidationCheckTime = WorldInfo.TimeSeconds; AimStart = GetSafeStartTraceLocation(); AimDir = vector( GetAdjustedAim(AimStart) ); bAlteredTargets = false; for( i = 0; i < LockedTargets.Length; ++i ) { // For speed don't bother checking a target we just locked if( RecentlyLocked != none && RecentlyLocked == LockedTargets[i] ) { continue; } bShouldRemoveTarget = false; if( LockedTargets[i] == none || !LockedTargets[i].IsAliveAndWell() ) { bShouldRemoveTarget = true; } else { TargetLoc = GetLockedTargetLoc( LockedTargets[i] ); if( AimDir dot Normal(LockedTargets[i].Location - AimStart) >= MaxLockMaintainFOVDotThreshold ) { HitActor = class'KFAIController'.static.ActorBlockTest( self, TargetLoc, AimStart,, true, true ); if( HitActor != none && HitActor != LockedTargets[i] ) { bShouldRemoveTarget = true; } } else { bShouldRemoveTarget = true; } } // A target was invalidated, remove it from the list if( bShouldRemoveTarget ) { LockedTargets.Remove( i, 1 ); --i; bAlteredTargets = true; continue; } } // Plays sound/FX when losing a target lock, but only if we didn't play a lock on this frame if( bAlteredTargets && RecentlyLocked == none ) { PlayTargetLostEffects(); } return bAlteredTargets; } /** Synchronizes our locked targets with the server */ reliable server function ServerSyncLockedTargets( Pawn TargetPawns[MAX_LOCKED_TARGETS] ) { local int i; LockedTargets.Length = 0; for( i = 0; i < MAX_LOCKED_TARGETS; ++i ) { if (TargetPawns[i] != none) { LockedTargets.AddItem(TargetPawns[i]); } } } /** Adjusts our destination target impact location */ static simulated function vector GetLockedTargetLoc( Pawn P ) { // Go for the chest, but just in case we don't have something with a chest bone we'll use collision and eyeheight settings if( P.Mesh.SkeletalMesh != none && P.Mesh.bAnimTreeInitialised ) { if( P.Mesh.MatchRefBone('Spine2') != INDEX_NONE ) { return P.Mesh.GetBoneLocation( 'Spine2' ); } else if( P.Mesh.MatchRefBone('Spine1') != INDEX_NONE ) { return P.Mesh.GetBoneLocation( 'Spine1' ); } return P.Mesh.GetPosition() + ((P.CylinderComponent.CollisionHeight + (P.BaseEyeHeight * 0.5f)) * vect(0,0,1)) ; } // General chest area, fallback return P.Location + ( vect(0,0,1) * P.BaseEyeHeight * 0.75f ); } simulated function ZoomIn(bool bAnimateTransition, float ZoomTimeToGo) { super.ZoomIn(bAnimateTransition, ZoomTimeToGo); if (IronsightsZoomInSound != none && Instigator != none && Instigator.IsLocallyControlled()) { IronsightsComponent.PlayEvent(IronsightsZoomInSound, false); } } /** Clear all locked targets when zooming out, both server and client */ simulated function ZoomOut( bool bAnimateTransition, float ZoomTimeToGo ) { super.ZoomOut( bAnimateTransition, ZoomTimeToGo ); if (IronsightsZoomOutSound != none && Instigator != none && Instigator.IsLocallyControlled()) { IronsightsComponent.PlayEvent(IronsightsZoomOutSound, false); } // Play a target lost effect if we're clearing targets on the way out if( Instigator.IsLocallyControlled() && LockedTargets.Length > 0 ) { PlayTargetLostEffects(); } LockedTargets.Length = 0; } /** Play FX or sounds when locking on to a new target */ simulated function PlayTargetLockOnEffects() { if( Instigator != none && Instigator.IsHumanControlled() ) { PlaySoundBase( LockAcquiredSoundFirstPerson, true ); } } /** Play FX or sounds when losing a target lock */ simulated function PlayTargetLostEffects() { if( Instigator != none && Instigator.IsHumanControlled() ) { PlaySoundBase( LockLostSoundFirstPerson, true ); } } /********************************************************************************************* * @name Projectile Spawning **********************************************************************************************/ /** Spawn projectile is called once for each rocket fired. In burst mode it will cycle through targets until it runs out */ simulated function KFProjectile SpawnProjectile( class KFProjClass, vector RealStartLoc, vector AimDir ) { local KFProj_Rocket_Seeker6 SeekerProj; if( CurrentFireMode == GRENADE_FIREMODE ) { return super.SpawnProjectile( KFProjClass, RealStartLoc, AimDir ); } // We need to set our target if we are firing from a locked on position if( bUsingSights && CurrentFireMode == ALTFIRE_FIREMODE && LockedTargets.Length > 0 ) { // We'll aim our rocket at a target here otherwise we will spawn a dumbfire rocket at the end of the function if( LockedTargets.Length > 0 ) { // Spawn our projectile and set its target SeekerProj = KFProj_Rocket_Seeker6( super.SpawnProjectile(KFProjClass, RealStartLoc, AimDir) ); if( SeekerProj != none ) { //Seek to new target, then remove from list. Always use first target in the list for new fire. SeekerProj.SetLockedTarget( KFPawn(LockedTargets[0]) ); LockedTargets.Remove(0, 1); return SeekerProj; } } return None; } return super.SpawnProjectile( KFProjClass, RealStartLoc, AimDir ); } /********************************************************************************************* * @name Targeting HUD -- Partially adapted from KFWeap_Rifle_RailGun **********************************************************************************************/ /** Handle drawing our custom lock on HUD */ simulated function DrawHUD( HUD H, Canvas C ) { local int i; if( !bUsingSights || LockedTargets.Length == 0 ) { return; } // Draw target locked icons C.EnableStencilTest( true ); for( i = 0; i < LockedTargets.Length; ++i ) { if( LockedTargets[i] != none ) { DrawTargetingIcon( C, i ); } } C.EnableStencilTest( false ); } /** Draws a targeting icon for each one of our locked targets */ simulated function DrawTargetingIcon( Canvas Canvas, int Index ) { local vector WorldPos, ScreenPos; local float IconSize, IconScale; // Project world pos to canvas WorldPos = GetLockedTargetLoc( LockedTargets[Index] ); ScreenPos = Canvas.Project( WorldPos );//WorldToCanvas(Canvas, WorldPos); // calculate scale based on resolution and distance IconScale = fMin( float(Canvas.SizeX) / 1024.f, 1.f ); // Scale down up to 40 meters away, with a clamp at 20% size IconScale *= fClamp( 1.f - VSize(WorldPos - Instigator.Location) / 4000.f, 0.2f, 1.f ); // Apply size scale IconSize = 200.f * IconScale; ScreenPos.X -= IconSize / 2.f; ScreenPos.Y -= IconSize / 2.f; // Off-screen check if( ScreenPos.X < 0 || ScreenPos.X > Canvas.SizeX || ScreenPos.Y < 0 || ScreenPos.Y > Canvas.SizeY ) { return; } Canvas.SetPos( ScreenPos.X, ScreenPos.Y ); // Draw the icon Canvas.DrawTile( LockedOnIcon, IconSize, IconSize, 0, 0, LockedOnIcon.SizeX, LockedOnIcon.SizeY, LockedIconColor ); } /********************************************************************************************* * State WeaponSingleFiring * Fire must be released between every shot. *********************************************************************************************/ simulated state WeaponSingleFiring { simulated function BeginState( Name PrevStateName ) { LockedTargets.Length = 0; super.BeginState( PrevStateName ); } } /********************************************************************************************* * State WeaponBurstFiring * Fires a burst of bullets. Fire must be released between every shot. *********************************************************************************************/ simulated state WeaponBurstFiring { simulated function int GetBurstAmount() { // Clamp our bursts to either the number of targets or how much ammo we have remaining return Clamp( LockedTargets.Length, 1, AmmoCount[GetAmmoType(CurrentFireMode)] ); } /** Overridden to apply scaled recoil when in multi-rocket mode */ simulated function ModifyRecoil( out float CurrentRecoilModifier ) { super.ModifyRecoil( CurrentRecoilModifier ); CurrentRecoilModifier *= BurstFireRecoilModifier; } simulated function bool ShouldRefire() { return LockedTargets.Length > 0; } simulated function FireAmmunition() { super.FireAmmunition(); if (Role < ROLE_Authority) { LockedTargets.Remove(0, 1); } } simulated event EndState( Name NextStateName ) { LockedTargets.Length = 0; super.EndState( NextStateName ); } } defaultproperties { ForceReloadTime=0.4f // Inventory InventoryGroup=IG_Primary GroupPriority=100 InventorySize=8 // 9 //10 WeaponSelectTexture=Texture2D'WEP_UI_SeekerSix_TEX.UI_WeaponSelect_SeekerSix' // FOV MeshFOV=86 MeshIronSightFOV=65 PlayerIronSightFOV=70 PlayerSprintFOV=95 // Depth of field DOF_FG_FocalRadius=50 DOF_FG_MaxNearBlurSize=2.5 // Zooming/Position PlayerViewOffset=(X=20.0,Y=5,Z=-5) FastZoomOutTime=0.2 // Content PackageKey="SeekerSix" FirstPersonMeshName="WEP_1P_SeekerSix_MESH.Wep_1stP_SeekerSix_Rig" FirstPersonAnimSetNames(0)="WEP_1P_SeekerSix_ANIM.Wep_1stP_SeekerSix_Anim" PickupMeshName="WEP_3P_SeekerSix_MESH.Wep_3rdP_SeekerSix_Pickup" AttachmentArchetypeName="WEP_SeekerSix_ARCH.Wep_SeekerSix_3P" MuzzleFlashTemplateName="WEP_SeekerSix_ARCH.Wep_SeekerSix_MuzzleFlash" // Target Locking MinTargetDistFromCrosshairSQ=2500.0f // 0.5 meters TimeBetweenLockOns=0.06f TargetValidationCheckInterval=0.1f MaxLockMaintainFOVDotThreshold=0.36f // LockOn Visuals LockedOnIcon=Texture2D'Wep_Scope_TEX.Wep_1stP_Yellow_Red_Target' LockedIconColor=(R=1.f, G=0.f, B=0.f, A=0.5f) // Lock On/Lost Sounds LockAcquiredSoundFirstPerson=AkEvent'WW_WEP_SA_Railgun.Play_Railgun_Scope_Locked' LockLostSoundFirstPerson=AkEvent'WW_WEP_SA_Railgun.Play_Railgun_Scope_Lost' // Zooming/Position IronSightPosition=(X=0,Y=-0.065,Z=-0.31) // Ammo MagazineCapacity[0]=6 SpareAmmoCapacity[0]=84 InitialSpareMags[0]=3 AmmoPickupScale[0]=2.0 bCanBeReloaded=true bReloadFromMagazine=true // Recoil maxRecoilPitch=900 minRecoilPitch=775 maxRecoilYaw=500 minRecoilYaw=-500 RecoilRate=0.085 RecoilBlendOutRatio=0.35 RecoilMaxYawLimit=500 RecoilMinYawLimit=65035 RecoilMaxPitchLimit=1500 RecoilMinPitchLimit=64785 RecoilISMaxYawLimit=50 RecoilISMinYawLimit=65485 RecoilISMaxPitchLimit=500 RecoilISMinPitchLimit=65485 RecoilViewRotationScale=0.8 FallingRecoilModifier=1.5 HippedRecoilModifier=1.25 BurstFireRecoilModifier=0.15f // Reduce recoil between rockets when in burst mode // DEFAULT_FIREMODE FireModeIconPaths(DEFAULT_FIREMODE)=Texture2D'UI_FireModes_TEX.UI_FireModeSelect_Rocket' FiringStatesArray(DEFAULT_FIREMODE)=WeaponFiring WeaponFireTypes(DEFAULT_FIREMODE)=EWFT_Projectile WeaponProjectiles(DEFAULT_FIREMODE)=class'KFProj_Rocket_Seeker6' FireInterval(DEFAULT_FIREMODE)=+0.35 InstantHitDamage(DEFAULT_FIREMODE)=125 //140 // 120.0 //100.00 InstantHitDamageTypes(DEFAULT_FIREMODE)=class'KFDT_Ballistic_Seeker6Impact' Spread(DEFAULT_FIREMODE)=0.025 FireOffset=(X=20,Y=4.0,Z=-3) // ALT_FIREMODE FireModeIconPaths(ALTFIRE_FIREMODE)= Texture2D'UI_SecondaryAmmo_TEX.UI_FireModeSelect_AutoTarget' FiringStatesArray(ALTFIRE_FIREMODE)=WeaponBurstFiring WeaponFireTypes(ALTFIRE_FIREMODE)=EWFT_Projectile WeaponProjectiles(ALTFIRE_FIREMODE)=class'KFProj_Rocket_Seeker6' FireInterval(ALTFIRE_FIREMODE)=+0.3 //0.1 InstantHitDamage(ALTFIRE_FIREMODE)=125 //140 // 120.0 //100.00 InstantHitDamageTypes(ALTFIRE_FIREMODE)=class'KFDT_Ballistic_Seeker6Impact' Spread(ALTFIRE_FIREMODE)=0.025 AmmoCost(ALTFIRE_FIREMODE)=1 // BASH_FIREMODE InstantHitDamageTypes(BASH_FIREMODE)=class'KFDT_Bludgeon_Seeker6' InstantHitDamage(BASH_FIREMODE)=29 // Fire Effects WeaponFireSnd(DEFAULT_FIREMODE)=(DefaultCue=AkEvent'WW_WEP_Seeker_6.Play_WEP_Seeker_6_Fire_3P', FirstPersonCue=AkEvent'WW_WEP_Seeker_6.Play_WEP_Seeker_6_Fire_1P') WeaponFireSnd(ALTFIRE_FIREMODE)=(DefaultCue=AkEvent'WW_WEP_Seeker_6.Play_WEP_Seeker_6_Fire_3P', FirstPersonCue=AkEvent'WW_WEP_Seeker_6.Play_WEP_Seeker_6_Fire_1P') WeaponDryFireSnd(DEFAULT_FIREMODE)=AkEvent'WW_WEP_SA_RPG7.Play_WEP_SA_RPG7_DryFire' WeaponDryFireSnd(ALTFIRE_FIREMODE)=AkEvent'WW_WEP_SA_RPG7.Play_WEP_SA_RPG7_DryFire' // Animation bHasFireLastAnims=true IdleFidgetAnims=(Guncheck_v1, Guncheck_v2) //BonesToLockOnEmpty=(RW_Grenade1) // Attachments bHasIronSights=true bHasFlashlight=false AssociatedPerkClasses(0)=class'KFPerk_Demolitionist' WeaponFireWaveForm=ForceFeedbackWaveform'FX_ForceFeedback_ARCH.Gunfire.Heavy_Recoil_SingleShot' // Audio Begin Object Class=AkComponent name=IronsightsComponent0 bForceOcclusionUpdateInterval=true OcclusionUpdateInterval=0.f // never update occlusion for footsteps bStopWhenOwnerDestroyed=true End Object IronsightsComponent=IronsightsComponent0 Components.Add(IronsightsComponent0) IronsightsZoomInSound=AkEvent'WW_WEP_Seeker_6.Play_Seeker_6_Iron_In' IronsightsZoomOutSound=AkEvent'WW_WEP_Seeker_6.Play_Seeker_6_Iron_In_Out' // Weapon Upgrade stat boosts //WeaponUpgrades[1]=(IncrementDamage=1.125f,IncrementWeight=1) WeaponUpgrades[1]=(Stats=((Stat=EWUS_Damage0, Scale=1.125f), (Stat=EWUS_Damage1, Scale=1.125f), (Stat=EWUS_Weight, Add=1))) }