//============================================================================= // KFFlashlightAttachment //============================================================================= // Attach and manage flash light components (light, cone, etc..) to a // weapon in 1st or 3rd person //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // - Andrew "Strago" Ladenberger //============================================================================= class KFFlashlightAttachment extends Object hidecategories(Object) config(Game) native(Effect); var config bool bSkipBestFlashlightCheck; /** Cached parent (attachment) mesh */ var protected transient SkeletalMeshComponent OwnerMesh; /** Is this flashlight turned on? */ var protected transient bool bEnabled; /** Initialized the first time the light is turned on */ var protected transient bool bLightInitialized; /********************************************************************************************* * @name Attachments ********************************************************************************************* */ /** Spot Light */ var() SpotLightComponent LightTemplate; var() transient SpotLightComponent Light; /** Light Cone Mesh*/ var() StaticMesh LightConeMesh; var StaticMeshComponent LightConeMeshComp; /** Mesh attachment (always visible) */ var() StaticMesh AttachmentMesh; var transient StaticMeshComponent AttachmentMeshComp; /** Socket name to attach the flashlight to */ var() name FlashlightSocketName; /** Within this radius don't turn off a teammate's flashlight unless ours is turned on */ var float TeammateSwitchRadius; var float TeammateSwitchTimer; /** Debug logging */ var bool bDebug; /** Maximum distance to warn AI */ var protected const float MaxAIWarningDistSQ; /** Maximum distance from center of line to warn AI */ var protected const float MaxAIWarningDistFromPointSQ; /** Create/Attach flashlight components */ function AttachFlashlight(SkeletalMeshComponent Mesh, optional name SocketNameOverride) { if ( OwnerMesh != None && OwnerMesh != Mesh ) { return; // unsupported attachment } OwnerMesh = Mesh; // Allow code to override attachment socket if ( SocketNameOverride != '' ) { FlashlightSocketName = SocketNameOverride; } // The AttachmentMesh must be attached right away. However, the lights are only created // if/when the flashlight is turned on for the first time if ( AttachmentMesh != None ) { if ( AttachmentMeshComp == None ) { AttachmentMeshComp = new(self) class'StaticMeshComponent'; AttachmentMeshComp.CastShadow = FALSE; AttachmentMeshComp.SetStaticMesh(AttachmentMesh); } else { // Cleanup previous attachment. For example, during customization mesh swap the // link to parent is lost, but the child link is retained for some reason. Mesh.DetachComponent(AttachmentMeshComp); } AttachFlashlightComponent(Mesh, AttachmentMeshComp); if ( IsOwnerFirstPerson() ) { SetFirstPersonVisibility(true); } } SetLightingChannels(Mesh.LightingChannels); } /** Need to reattach when owner mesh changes (e.g. Customization) */ function Reattach() { if ( AttachmentMeshComp != None && !AttachmentMeshComp.bAttached ) { // Cleanup previous attachment. For example, during customization mesh swap the // link to parent is lost, but the child link is retained for some reason. OwnerMesh.DetachComponent(AttachmentMeshComp); AttachFlashlightComponent(OwnerMesh, AttachmentMeshComp); } } /** Remove/Detach flashlight components */ function DetachFlashlight() { if ( Light != none ) { Light.DetachFromAny(); } if ( LightConeMeshComp != none ) { LightConeMeshComp.DetachFromAny(); } if ( AttachmentMeshComp != None ) { AttachmentMeshComp.DetachFromAny(); } } /** Toggle flash light components */ protected function SetEnabled(bool bNewEnabled) { if ( bNewEnabled && !bLightInitialized ) { InitializeLight(); } if ( Light != None ) { Light.SetEnabled(bNewEnabled); } if ( LightConeMeshComp != None ) { LightConeMeshComp.SetHidden(!bNewEnabled || IsOwnerFirstPerson()); } bEnabled = bNewEnabled; if( OwnerMesh.Outer != none && class'WorldInfo'.static.GetWorldInfo().NetMode != NM_Client ) { if( bEnabled ) { Timer_WarnAI(); Actor(OwnerMesh.Outer).SetTimer( 1.f, true, nameOf(Timer_WarnAI), self ); } else { Actor(OwnerMesh.Outer).ClearTimer( nameOf(Timer_WarnAI), self ); } } `log("Turning flashlight"@bNewEnabled@"for teammate:"@OwnerMesh.Outer, bDebug); } /** Warns AI when flaslight is on */ function Timer_WarnAI() { local Pawn Instigator, P; local KFWeapon OwnerWeapon; local KFPawn_Monster HitMonster; local vector Direction, Projection, DangerPoint; // Get owner pawn Instigator = Pawn( OwnerMesh.Outer ); if( Instigator == none ) { return; } // Get owner weapon. Don't process AI warnings if weapon is already doing so. OwnerWeapon = KFWeapon( Instigator.Weapon ); if( OwnerWeapon != none && OwnerWeapon.IsWarningAI() ) { return; } Direction = vector( Instigator.GetBaseAimRotation() ); // Iterate through pawns and find AI we want to warn foreach Instigator.WorldInfo.AllPawns( class'Pawn', P ) { if( P.GetTeamNum() != Instigator.GetTeamNum() && !P.IsHumanControlled() && P.IsAliveAndWell() ) { // Determine if AI is within range as well as within our field of view Projection = P.Location - Instigator.Location; if( VSizeSQ(Projection) < MaxAIWarningDistSQ ) { PointDistToLine( P.Location, Direction, Instigator.Location, DangerPoint ); if( VSizeSQ(DangerPoint - P.Location) < MaxAIWarningDistFromPointSQ ) { // Tell the AI to evade away from the DangerPoint HitMonster = KFPawn_Monster( P ); if( HitMonster != none && HitMonster.MyKFAIC != none ) { HitMonster.MyKFAIC.ReceiveLocationalWarning( DangerPoint, Instigator.Location, self ); } } } } } } /** Called once the first time the light is activated. Seperated from AttachFlashlight and the AttachmentMesh */ protected function InitializeLight() { if ( OwnerMesh == None ) { `log("Invalid mesh for flashlight"@self); return; } if ( Light == None && LightTemplate != None ) { Light = new(self) Class'SpotLightComponent' (LightTemplate); AttachFlashlightComponent(OwnerMesh, Light); } if ( LightConeMesh != None ) { LightConeMeshComp.SetStaticMesh(LightConeMesh); AttachFlashlightComponent(OwnerMesh, LightConeMeshComp); } bLightInitialized = true; if ( IsOwnerFirstPerson() ) { SetFirstPersonVisibility(true); } } /** attachment helper */ private function AttachFlashlightComponent(SkeletalMeshComponent ParentMesh, ActorComponent Attachment) { ParentMesh.AttachComponentToSocket(Attachment, FlashlightSocketName); } /** Cleanup */ simulated function OwnerDied() { if ( bEnabled ) { SetEnabled(false); } // make sure SpotLightComponent is removed (in case of 1st person camera attachment) if ( Light != None ) { Light.DetachFromAny(); } } /** Set the lighting channels on all the appropriate pawn meshes */ simulated function SetLightingChannels(const out LightingChannelContainer NewLightingChannels) { if ( AttachmentMeshComp != None ) { AttachmentMeshComp.SetLightingChannels(NewLightingChannels); } } /********************************************************************************************* * @name 1st person attachment ********************************************************************************************* */ /** attachment helper */ private function bool IsOwnerFirstPerson() { local Pawn P; P = Pawn(OwnerMesh.Outer); return (P != None) ? P.IsFirstPerson() : false; } /** Special handling for components attached to character mesh while in 1st person */ simulated function SetFirstPersonVisibility(bool bFirstPerson) { local KFPawn P; if ( bLightInitialized ) { P = KFPawn(OwnerMesh.Outer); if ( Light != None && P != None ) { if ( bFirstPerson ) { if( P.Controller != none && PlayerController(P.Controller) != none && PlayerController(P.Controller).PlayerCamera != none ) { PlayerController(P.Controller).PlayerCamera.AttachComponent(Light); } //Light.SetTranslation(vect(0, -100, -50)); } else { AttachFlashlightComponent(P.Mesh, Light); //Light.SetTranslation(vect(0, 0, 0)); } } if ( LightConeMeshComp != None ) { LightConeMeshComp.SetHidden(!bEnabled || bFirstPerson); } } if ( AttachmentMeshComp != None ) { AttachmentMeshComp.SetHidden(bFirstPerson); } } /********************************************************************************************* * @name Limit 1 flashlight per client ********************************************************************************************* */ simulated function UpdateFlashlightFor(KFPawn_Human InPawn) { local PlayerController PC; local KFPawn_Human P; if ( bSkipBestFlashlightCheck ) { InPawn.Flashlight.SetEnabled(InPawn.bFlashlightOn); return; } if ( !InPawn.bFlashlightOn ) { // If the active flashlight is being turned off, choose another if ( InPawn.Flashlight.bEnabled ) { InPawn.Flashlight.SetEnabled(false); ChooseBestFlashlight(); } } else { PC = class'WorldInfo'.static.GetWorldInfo().GetALocalPlayerController(); if ( PC == None ) { return; } // If local player (or spectator) uses flashlight turn off all others if ( PC.ViewTarget == InPawn ) { foreach PC.WorldInfo.AllPawns( class'KFPawn_Human', P ) { if ( P.Flashlight.bEnabled ) { P.Flashlight.SetEnabled(false); } } InPawn.Flashlight.SetEnabled(true); } // If nobody else has a flash light choose one now else if ( !PC.IsTimerActive(nameof(ChooseBestFlashlightTimer), self) ) { ChooseBestFlashlight(); PC.SetTimer(TeammateSwitchTimer, true, nameof(ChooseBestFlashlightTimer), self); } else { // timer is already going... just wait } } } /** Called by local player on looping timer while another teammate has light on */ simulated function ChooseBestFlashlightTimer() { ChooseBestFlashlight(); } /** Choose a nearby teammate to enable flashlight for */ simulated function ChooseBestFlashlight() { local PlayerController PC; local KFPawn_Human P, BestPawn; local array DisableList; local float BestDistSq, DistSq; local float TeammateSwitchRadiusSq; local int i; TeammateSwitchRadiusSq = Square(TeammateSwitchRadius); PC = class'WorldInfo'.static.GetWorldInfo().GetALocalPlayerController(); // find other players that are using their flashlight and choose the nearest // @todo: move to native for performance, or better yet add a AllHumans iterator foreach PC.WorldInfo.AllPawns( class'KFPawn_Human', P ) { if ( !P.bFlashlightOn || !P.IsAliveAndWell() ) continue; DistSq = VSizeSq(P.Location - PC.ViewTarget.Location); // if pawn has flashlight on we may be done if ( P.Flashlight.bEnabled ) { if ( PC.ViewTarget == P ) { `log("Flashlight staying on for view target", bDebug); return; // leave flashlight on } else if ( DistSq < TeammateSwitchRadiusSq ) { `log("Flashlight staying on for"@P, bDebug); return; // leave flashlight on } DisableList.AddItem(P); } // rate by distance if ( BestPawn == None || DistSq < BestDistSq ) { BestPawn = P; BestDistSq = DistSq; } } // If no players have their light on we can stop the timer if ( BestPawn == None ) { PC.ClearTimer(nameof(ChooseBestFlashlightTimer), self); } // Enable flashlight on bestPawn and disable on others else if ( BestPawn != None && !BestPawn.Flashlight.bEnabled ) { BestPawn.Flashlight.SetEnabled(true); // Disable others (Length "should" always be 1) for(i = 0; i < DisableList.Length; i++) { DisableList[i].Flashlight.SetEnabled(false); } } } defaultproperties { Begin Object Class=LightFunction Name=FlashLightFunction_0 SourceMaterial=Material'FX_Mat_Lib.VFX_Flashlight_PM' End Object Begin Object Class=SpotLightComponent Name=FlashlightTemplate_0 Brightness=0.5 InnerConeAngle=5 OuterConeAngle=20 Radius=3000 CastShadows=FALSE CastStaticShadows=FALSE CastDynamicShadows=FALSE bCastPerObjectShadows=false ForceCastDynamicShadows=FALSE Function=FlashLightFunction_0 LightingChannels=(Indoor=TRUE,Outdoor=TRUE,bInitialized=TRUE) bEnabled=FALSE bUpdateOwnerRenderTime=TRUE End Object LightTemplate=FlashlightTemplate_0 Begin Object Class=StaticMeshComponent Name=LightConeComp_0 CastShadow=FALSE bAcceptsStaticDecals=FALSE bAcceptsDecals=FALSE CollideActors=FALSE BlockActors=FALSE BlockZeroExtent=FALSE BlockNonZeroExtent=FALSE BlockRigidBody=FALSE End Object LightConeMeshComp=LightConeComp_0 FlashlightSocketName=FlashLight TeammateSwitchRadius=1500 // 15m TeammateSwitchTimer=10.f MaxAIWarningDistSQ=4000000 MaxAIWarningDistFromPointSQ=16384 }