//=============================================================================
// KFLaserSightAttachment
//=============================================================================
// Attach and manage laser sight to a weapon in 1st person
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Sakib Saikia
//=============================================================================

class KFLaserSightAttachment extends Object
	hidecategories(Object)
	native(Effect);

/** Distance at which we should start scaling the dot size and depth bias (5m) */
var() float LaserDotLerpStartDistance;
/** Distance at which we should stop scaling the dot size and depth bias (60m) */
var() float LaserDotLerpEndDistance;
/** Max scale is clamped at 20x */
var() float LaserDotMaxScale;
/** How much to pull the laser dot back to make sure it doesn't clip through what it hit */
var() float LaserDotDepthBias;

/*********************************************************************************************
 * @name	Attachments
 ********************************************************************************************* */

/** Static Mesh */
var() StaticMesh LaserDotMesh;
var transient StaticMeshComponent LaserDotMeshComp;

/** Laser Sight Mesh */
var()		  SkeletalMesh				LaserSightMesh;
var transient KFSkeletalMeshComponent	LaserSightMeshComp;

/** Laser Mesh */
var()		  SkeletalMesh				LaserBeamMesh;
var transient KFSkeletalMeshComponent	LaserBeamMeshComp;

/** Socket to attach the LaserSight to */
var() name LaserSightSocketName;
var() float LaserSightRange;

/** Specifies how much animation to blend in for the laser dot.
	If 0, the dot will match the aim direction perfectly.
	If 1, the dot will be completely controlled by animation
	NOTE: First person only.
 */
var float AnimWeight;

/** Specifies blending rate between aim and animation */
var() float AnimBlendRate;

/** How strongly the laser sight should adhere to the aim direction.
	Used to blend in and out of the weapon's active state
 */
var transient 			float 			LaserSightAimStrength;
var transient 			float 			DesiredAimStrength;

// Use for automatic weapons, then the Laser Dot will always steer to the hit location no matter what
var transient			bool			bForceDotToMatch;

var transient bool IsVisible;

/** Create/Attach lasersight components */
function AttachLaserSight(SkeletalMeshComponent OwnerMesh, bool bFirstPerson, optional name SocketNameOverride)
{
    local KFSkeletalMeshComponent KFMesh;

	if ( OwnerMesh == None )
	{
		`log("Invalid mesh for laser sight " @self);
		return;
	}

	// Allow code to override attachment socket
	if ( SocketNameOverride != '' )
	{
		LaserSightSocketName = SocketNameOverride;
	}

	if ( LaserDotMesh != None && bFirstPerson )
	{
		LaserDotMeshComp.SetStaticMesh(LaserDotMesh);
		OwnerMesh.AttachComponentToSocket(LaserDotMeshComp, LaserSightSocketName);
	}

	if ( LaserSightMesh != None )
	{
        LaserSightMeshComp.SetSkeletalMesh(LaserSightMesh);
		OwnerMesh.AttachComponentToSocket(LaserSightMeshComp, LaserSightSocketName);
        if( bFirstPerson )
        {
            LaserSightMeshComp.SetDepthPriorityGroup(SDPG_Foreground);
        }
	}

	if ( LaserBeamMesh != None )
	{
		LaserBeamMeshComp.SetSkeletalMesh(LaserBeamMesh);
		OwnerMesh.AttachComponentToSocket(LaserBeamMeshComp, LaserSightSocketName);

		if( bFirstPerson )
		{
			LaserBeamMeshComp.SetDepthPriorityGroup(SDPG_Foreground);
		}
	}

    KFMesh =  KFSkeletalMeshComponent(OwnerMesh);

	// If attaching to a mesh with a custom FOV
	if (KFMesh != none && KFMesh.FOV > 0)
	{
		SetMeshFOV( KFMesh.FOV );
	}
}

/** Set the FOV of the laser sight mesh */
simulated function SetMeshFOV( float NewFOV )
{
	if( LaserBeamMeshComp.SkeletalMesh != none )
	{
        LaserBeamMeshComp.SetFOV( NewFOV );
    }

    if( LaserSightMeshComp.SkeletalMesh != none )
    {
	   LaserSightMeshComp.SetFOV( NewFOV );
	}
}

/** Set the lighting channels on all the appropriate weapon attachment mesh(es) */
simulated function SetMeshLightingChannels(LightingChannelContainer NewLightingChannels)
{
	if( LaserSightMeshComp.SkeletalMesh != none  )
	{
		LaserSightMeshComp.SetLightingChannels(NewLightingChannels);
	}
}

simulated event ChangeVisibility(bool bVisible)
{
	IsVisible = bVisible;

    LaserDotMeshComp.SetHidden(!bVisible);
    LaserSightMeshComp.SetHidden(!bVisible);
    LaserBeamMeshComp.SetHidden(!bVisible);
}

/** 
 * Update function called from currently equipped 1st person weapon 
 * @todo: move to c++?
 */
simulated function Update(float DeltaTime, KFWeapon OwningWeapon)
{
	local vector TraceStart, TraceEnd;
	local vector InstantTraceHitLocation, InstantTraceHitNormal;
	local vector HitLocation, HitNormal;
	local vector TraceAimDir;
	local vector SocketSpaceNewTraceDir, WorldSpaceNewTraceDir;
	local vector SocketSpaceAimLocation, SocketSpaceAimDir;
	local Actor	HitActor;
	local rotator SocketRotation;
	local matrix SocketToWorldTransform;
	local vector DirA, DirB;
	local Quat Q;
	local TraceHitInfo		HitInfo;

	if (IsVisible == false)
	{
		return;
	}

	if( OwningWeapon != None &&
		OwningWeapon.Instigator != None &&
		OwningWeapon.Instigator.Weapon == OwningWeapon &&
		OwningWeapon.Instigator.IsFirstPerson() )
	{
		UpdateFirstPersonAimStrength(DeltaTime, OwningWeapon);

		// This is where we would start an instant trace
		TraceStart = OwningWeapon.Instigator.GetWeaponStartTraceLocation();
		TraceAimDir = Vector(OwningWeapon.Instigator.GetAdjustedAimFor( OwningWeapon, TraceStart ));

		// Do aim calculations only when weapon is in active state
		if( LaserSightAimStrength > 0.f )
		{
			// Simulate an instant trace where weapon is aiming, Get hit info.
			// Take minimal part of CalcWeaponFire()
			TraceEnd = TraceStart + TraceAimDir * LaserSightRange;
			HitActor = OwningWeapon.GetTraceOwner().Trace(InstantTraceHitLocation, InstantTraceHitNormal, TraceEnd, TraceStart, TRUE, vect(0,0,0), HitInfo, OwningWeapon.TRACEFLAG_Bullet);

			//OwningWeapon.MySkelMesh.GetSocketWorldLocationAndRotation(LaserSightSocketName, SocketLocation);
			//OwningWeapon.DrawDebugLine(SocketLocation, InstantTraceHitLocation, 255, 255, 0, FALSE);

			if( HitActor != None )
			{
				// If aim strength is not 100%, then blend in animation
				if( LaserSightAimStrength < 1.f )
				{
					if( OwningWeapon.MySkelMesh != None &&
						OwningWeapon.MySkelMesh.GetSocketWorldLocationAndRotation(LaserSightSocketName,  TraceStart,  SocketRotation) )
					{

						//OwningWeapon.DrawDebugLine(TraceStart, TraceStart + 1000*Vector(SocketRotation), 0, 255, 255, FALSE);

						SocketToWorldTransform = OwningWeapon.MySkelMesh.GetSocketMatrix(LaserSightSocketName);
						SocketSpaceAimLocation = InverseTransformVector(SocketToWorldTransform, InstantTraceHitLocation);

                		// Basically SocketSpaceAimLocation - (0,0,0)
                		SocketSpaceAimDir = Normal(SocketSpaceAimLocation);

						// Clamp the maximum aim adjustment for the AimDir so you don't get weird
						// cases where the aim dir is rotated away from the location where you
						// are aiming. This can happen if you are really close to an object.
						// Note : DirB is the socket face dir
						DirB = vect(1,0,0);
						DirA = SocketSpaceAimDir;

						if ( (DirA dot DirB) < class'KFWeapon'.const.MaxAimAdjust_Cos )
						{
							Q = QuatFromAxisAndAngle(Normal(DirB cross DirA), class'KFWeapon'.const.MaxAimAdjust_Angle );
							SocketSpaceAimDir = QuatRotateVector(Q,DirB);
						}

		                // Apply relative rotation to aim along desired direction
						SocketSpaceNewTraceDir = LaserSightAimStrength * SocketSpaceAimDir + (1.f - LaserSightAimStrength) * DirB;

						// Transform the direction to world space. Convert direction vector  in socket space to point
						// in world space and subtract the socket location to get direction vector in world space
						WorldSpaceNewTraceDir = TransformVector(SocketToWorldTransform, SocketSpaceNewTraceDir) - TraceStart;

						// Trace from socket along new trace direction
						TraceEnd = TraceStart + Normal(WorldSpaceNewTraceDir) * LaserSightRange;
						HitActor = OwningWeapon.GetTraceOwner().Trace(HitLocation, HitNormal, TraceEnd, TraceStart, TRUE, vect(0,0,0), HitInfo, OwningWeapon.TRACEFLAG_Bullet);

						if( HitActor != None )
						{
							// Unhide the dot mesh and make it point at given hit location
							// Note: SetHidden() checks for previous visibility state before reattaching
							LaserDotMeshComp.SetHidden(false);
							AimAt(HitLocation, HitNormal, OwningWeapon.MySkelMesh);
							//OwningWeapon.DrawDebugLine(HitLocation, HitLocation + 50*Normal(HitNormal), 0, 255, 0, FALSE);
						}
						else
						{
							// Hide the dot mesh if it didn't hit anything
							LaserDotMeshComp.SetHidden(true);
						}
	                }
				}
				else
				{
					// If aim strength is 100%, then just aim at the instant trace location
					LaserDotMeshComp.SetHidden(false);
					AimAt(InstantTraceHitLocation, InstantTraceHitNormal, OwningWeapon.MySkelMesh);
				}
			}
			else
			{
				// Hide the dot mesh if it didn't hit anything
				LaserDotMeshComp.SetHidden(true);
			}
		}
		else
		{
			// Weapon is in "Inactive" state when it is unequipped. Skip these
			// calculations when unequipped
			if( OwningWeapon.MySkelMesh != None &&
				OwningWeapon.MySkelMesh.GetSocketWorldLocationAndRotation(LaserSightSocketName,  TraceStart,  SocketRotation) )
			{
				DirA = vector(SocketRotation);
				DirB = TraceAimDir;

				// If we're off by more than 20 degrees just hide the dot.  This 
				// covers up an issue where the weapon FOV adjustment causes a desync
				if ( (DirA dot DirB) < 0.94f)
				{
					LaserDotMeshComp.SetHidden(true);
					return;
				}

				TraceEnd = TraceStart + vector(SocketRotation) * LaserSightRange;
				HitActor = OwningWeapon.GetTraceOwner().Trace(HitLocation, HitNormal, TraceEnd, TraceStart, TRUE, vect(0,0,0), HitInfo, OwningWeapon.TRACEFLAG_Bullet);
				if( HitActor != None )
				{
					// Unhide the dot mesh and make it point at given hit location
					// Note: SetHidden() checks for previous visibility state before reattaching
					LaserDotMeshComp.SetHidden(false);
					AimAt(HitLocation, HitNormal, OwningWeapon.MySkelMesh);
				}
				else
				{
					// Hide the dot mesh if it didn't hit anything
					LaserDotMeshComp.SetHidden(true);
				}
			}
		}
	}
}

function bool IsIdleFidgetAnimation(KFWeapon W, name AnimationName)
{
	local int i;

	for (i = 0; i < W.IdleFidgetAnims.Length; ++i)
	{
		if (AnimationName == W.IdleFidgetAnims[i])
		{
			return true;
		}
	}

	return false;
}

/** Determine how much to weigh screen center versus weapon socket */
function UpdateFirstPersonAImStrength(float DeltaTime, KFWeapon W)
{
	// aim at center of screen
	if ( W.IsInState('Active') &&
		// If additive bob is off we're playing a fidget animation and need to follow weapon
		W.IdleBobBlendNode != None && W.IdleBobBlendNode.Child2WeightTarget == 1.f )
	{
		DesiredAimStrength = 1.f - AnimWeight;
	}
	// we are forcing the dot to match, don't do while reloading though
	else if (bForceDotToMatch 
				&& (W.IsInstate('Reloading') == false
					&& W.IsInState('WeaponSprinting') == false
					&& IsIdleFidgetAnimation(W, W.WeaponAnimSeqNode.AnimSeqName) == false))
	{
		DesiredAimStrength = 1.f - AnimWeight;
	}
	// follow weapon
	else
	{
		DesiredAimStrength = 0.f;
	}

	if( LaserSightAimStrength < DesiredAimStrength )
	{
		LaserSightAimStrength = FMin(LaserSightAimStrength + AnimBlendRate * DeltaTime, DesiredAimStrength);
	}
	else if( LaserSightAimStrength > DesiredAimStrength )
	{
		LaserSightAimStrength = FMax(LaserSightAimStrength - AnimBlendRate * DeltaTime, DesiredAimStrength);
	}
}

/**
 * Draw the laser dot at the given world space location and oriented in the direction of the given hit normal
 */
function AimAt(vector HitLocation, vector HitNormal, SkeletalMeshComponent ParentMesh)
{
	local vector SocketSpaceAimLocation;
	local matrix SocketToWorldTransform;
	local float LaserDotScale;
	local vector SocketLocation;
	local vector SocketToHit;

    ParentMesh.GetSocketWorldLocationAndRotation(LaserSightSocketName, SocketLocation);

    // Pull the hit location back a bit to keep the laser dot from clipping
    SocketToHit = (HitLocation - SocketLocation) * LaserDotDepthBias;
    HitLocation = SocketLocation + SocketToHit;

	SocketToWorldTransform = ParentMesh.GetSocketMatrix(LaserSightSocketName);

	// Transform the aim location to the coordinate system of the laser sight socket
	SocketSpaceAimLocation = InverseTransformVector(SocketToWorldTransform, HitLocation);
	LaserDotMeshComp.SetTranslation(SocketSpaceAimLocation);

	// Scale laser dot based on distance clamped at 10x at 30m
	LaserDotScale = 1.f + (LaserDotMaxScale - 1.f) * FMax((SocketSpaceAimLocation.X - LaserDotLerpStartDistance)/(LaserDotLerpEndDistance - LaserDotLerpStartDistance), 0.f);
	LaserDotMeshComp.SetScale(LaserDotScale);

	// START DEBUG
//	`log("Distance = " $ SocketSpaceAimLocation.X @ "Scale = " $ LaserDotScale);
//	class'WorldInfo'.static.GetWorldInfo().DrawDebugLine(HitLocation, HitLocation + 50*Normal(HitNormal), 0, 0, 255, FALSE);
	// END DEBUG
}

/** 
 * Since 1st person weapons have a custom rendered FOV when we need accuracy 
 * GetSocketWorldLocationAndRotation() is not good enough.  This is not without
 * problems as it's expensive to compute and introduces a frame lag because
 * skeletal mesh FOV is adjusted on the render thread.
 */
native function bool GetFOVAdjustedLaserSocket(KFSkeletalMeshComponent Mesh, name InSocketName, out vector OutLocation, out rotator OutRotation);

defaultproperties
{
	Begin Object Class=StaticMeshComponent Name=LaserDotStaticMeshComponent_0
		CastShadow=FALSE
		CollideActors=FALSE
		BlockActors=FALSE
		BlockZeroExtent=FALSE
		BlockNonZeroExtent=FALSE
		BlockRigidBody=FALSE
		bAcceptsDecals=FALSE
		TickGroup=TG_PostAsyncWork
		DepthPriorityGroup=SDPG_Foreground // First person only
	End Object
	LaserDotMeshComp=LaserDotStaticMeshComponent_0

	Begin Object Class=KFSkeletalMeshComponent Name=LaserSightMeshComponent_0
		CollideActors=FALSE
		BlockActors=FALSE
		BlockZeroExtent=FALSE
		BlockNonZeroExtent=FALSE
		BlockRigidBody=FALSE
		bAcceptsDecals=FALSE
		bOwnerNoSee=true
		bOnlyOwnerSee=false
		AlwaysLoadOnClient=true
		AlwaysLoadOnServer=false
		MaxDrawDistance=4000
		bAcceptsDynamicDecals=FALSE
		CastShadow=true
		bCastDynamicShadow=true
		bUpdateSkelWhenNotRendered=false
		bIgnoreControllersWhenNotRendered=true
		bOverrideAttachmentOwnerVisibility=true
		// Default to outdoor. If indoor, this will be set when TWIndoorLightingVolume::Touch() event is received at spawn.
		LightingChannels=(Outdoor=TRUE,bInitialized=TRUE)
		TickGroup=TG_PostAsyncWork
	End Object
	LaserSightMeshComp=LaserSightMeshComponent_0

	Begin Object Class=KFSkeletalMeshComponent Name=LaserBeamMeshComp_0
		CastShadow=FALSE
	End Object
	LaserBeamMeshComp=LaserBeamMeshComp_0

	LaserSightRange=20000 //200m

	// Set AnimWeight to 0 as it causes the laser dot to diverge from the
	// aim location when you the player is right up against an obstace
	AnimWeight=0.f
	AnimBlendRate=3.f

    LaserDotLerpStartDistance=25.f
    LaserDotLerpEndDistance=6000.f
    LaserDotMaxScale=10.f
    LaserDotDepthBias=0.95f

	IsVisible=true

	bForceDotToMatch=false
}
