//=============================================================================
// KFImpactEffectManager
//=============================================================================
// Manages impact effects
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Andrew "Strago" Ladenberger
//=============================================================================

class KFImpactEffectManager extends Actor
	native(Effect)
	config(Game);

/** Information about hit effects for this class */
var KFImpactEffectInfo DefaultImpactEffects;

var float MaxImpactEffectDistance;
var float MaxDecalRangeSq;

var bool bAlignToSurfaceNormal;
var bool bSuppressSounds;

/** Decal Limits */
var globalconfig int MaxImpactEffectDecals;

/** Decal manager */
var transient DecalManager ImpactEffectDecalManager;

/** Data for most recent impact sound played (used to limit sounds) */
var transient AkEvent 	MostRecentImpactSound;
var transient vector 	MostRecentImpactLocation;
var transient float		MostRecentImpactTime;
var transient int		NumDuplicateImpactSounds;

/** Proximity within which to limit impact sounds */
var float MinRepeatImpactSoundDistanceSq;
/** Duration within which to limit impact sounds */
var float MinRepeatImpactSoundInterval;
/** Number of duplicate sounds to allow within time period and proximity */
var int MaxDuplicateImpactSounds;

event PostBeginPlay()
{
	Super.PostBeginPlay();

	// Create decal managers
	if( WorldInfo.NetMode != NM_DedicatedServer )
	{
		// Wound decal manager
		ImpactEffectDecalManager = Spawn(class'DecalManager', self,, vect(0,0,0), rot(0,0,0));
		ImpactEffectDecalManager.MaxActiveDecals = MaxImpactEffectDecals;
	}
}

/**
 * Spawn any effects that occur at the impact point.  It's called from the pawn.
 */
simulated function PlayImpactEffects(const vector HitLocation, const Pawn EffectInstigator, optional vector HitNormal, optional KFImpactEffectInfo CustomImpactEffects, optional bool bWorldImpactsOnly)
{
	local vector NewHitLoc, FireDir, WaterHitNormal;
	local Actor HitActor;
	local TraceHitInfo HitInfo;
	local MaterialImpactEffect ImpactEffect;
    local KFImpactEffectInfo ImpactEffectInfo;
	local KFFracturedMeshActor FracturedMeshActor;
	local int i;
	local bool bIsWeaponHandlingEffects;

	// allow optional parameter to override impact effects
	ImpactEffectInfo = (CustomImpactEffects != None) ? CustomImpactEffects : DefaultImpactEffects;	

	if( IsZero(HitNormal) && EffectInstigator != none )
	{
	   HitNormal = Normal(EffectInstigator.Location - HitLocation);
	}
	FireDir = -1 * HitNormal;

	if ( ImpactEffectInfo.BulletWhipSnd != None )
	{
		CheckBulletWhip(FireDir, HitLocation, EffectInstigator, ImpactEffectInfo);
	}

	if ( EffectInstigator != None && ImpactEffectIsRelevant(EffectInstigator, HitLocation, false, MaxImpactEffectDistance) )
	{
		if ( ImpactEffectInfo.bMakeSplash && !WorldInfo.bDropDetail && EffectInstigator.IsPlayerPawn() && EffectInstigator.IsLocallyControlled() )
		{
			HitActor = Trace(NewHitLoc, WaterHitNormal, HitLocation, EffectInstigator.Location+EffectInstigator.eyeheight*vect(0,0,1), true,, HitInfo, TRACEFLAG_PhysicsVolumes | TRACEFLAG_Bullet);
			if ( WaterVolume(HitActor) != None )
			{
				WorldInfo.MyEmitterPool.SpawnEmitter(ImpactEffectInfo.SplashEffectTemplate, NewHitLoc, rotator(vect(0,0,1)));
			}
		}

		bIsWeaponHandlingEffects = KFPawn(EffectInstigator).MyKFWeapon.bForceHandleImpacts;

		// Trace using the Instigator as the TraceOwner so that melee weapons don't collide with Instigator
		HitActor = EffectInstigator.Trace(NewHitLoc, HitNormal, (HitLocation - (HitNormal * 32)), HitLocation + (HitNormal * 32), !bWorldImpactsOnly,, HitInfo, TRACEFLAG_Bullet);
		if( HitActor != none && HitActor.bCanBeDamaged && HitActor.IsA('Pawn') && !bIsWeaponHandlingEffects )
		{
			return; // pawns impacts are handled by the pawn (see PlayTakeHitEffects)
		}

		// handle "partial" remote client simulation for fracture meshes
		if ( WorldInfo.NetMode == NM_Client && HitActor != none && HitActor.bCanBeDamaged && !EffectInstigator.IsLocallyControlled() )
		{
			FracturedMeshActor = KFFracturedMeshActor(HitActor);
			if ( FracturedMeshActor != None )
			{
				FracturedMeshActor.SimulateRemoteHit(HitLocation, HitNormal, HitInfo);
			}
		}

		// figure out the impact sound to use
		GetImpactEffect(HitInfo.PhysMaterial, ImpactEffect, ImpactEffectInfo);

		// Allow phys materials to override default impact effects
		if( ImpactEffect == ImpactEffectInfo.DefaultImpactEffect
			&& HitInfo.PhysMaterial != none )
		{
			// Impact effect override
			if( HitInfo.PhysMaterial.ImpactEffect != none )
			{
				ImpactEffect.ParticleTemplate = HitInfo.PhysMaterial.ImpactEffect;
			}				

			// Sound override
			if( HitInfo.PhysMaterial.ImpactSound != none )
			{
				ImpactEffect.Sound = HitInfo.PhysMaterial.ImpactSound;
			}
		}

		// Pawns handle their own hit effects
		if ( HitActor != None &&
			 ((Pawn(HitActor) == None || bIsWeaponHandlingEffects ) || Vehicle(HitActor) != None) &&
			 AllowImpactEffects(HitActor, HitLocation, HitNormal) )
		{
			if (ImpactEffect.ParticleTemplate != None)
			{
				if (!bAlignToSurfaceNormal)
				{
					HitNormal = normal(FireDir - ( 2 *  HitNormal * (FireDir dot HitNormal) ) ) ;
				}
				WorldInfo.MyEmitterPool.SpawnEmitter(ImpactEffect.ParticleTemplate, HitLocation, rotator(HitNormal), HitActor);
			}

			//Spawn any global emitters
			for (i = 0; i < ImpactEffectInfo.GlobalImpactEffectEffects.Length; ++i)
			{
				WorldInfo.MyEmitterPool.SpawnEmitter(ImpactEffectInfo.GlobalImpactEffectEffects[i], HitLocation, rotator(HitNormal), HitActor);
			}

			if ( !WorldInfo.bDropDetail
				&& (Pawn(HitActor) == None)
				&& (EffectInstigator != none && VSizeSQ(EffectInstigator.Location - HitLocation) < MaxDecalRangeSq)
				&& (((WorldInfo.GetDetailMode() != DM_Low) && !class'Engine'.static.IsSplitScreen()) || (EffectInstigator.IsLocallyControlled()
				&& EffectInstigator.IsHumanControlled())) )
			{
				SpawnImpactDecal(HitLocation, HitNormal, HitActor, ImpactEffect, HitInfo);
			}
		}

		if( ShouldPlayImpactSound(ImpactEffect.Sound, HitLocation) )
		{
			PlayImpactSound(EffectInstigator, HitLocation, FireDir, HitActor, ImpactEffect.Sound, ImpactEffectInfo);
		}
	}
}

/** Determines whether impact sound should be played or not, based on most recent impact */
simulated function bool ShouldPlayImpactSound( AkBaseSoundObject HitSound, vector HitLocation )
{
	if( HitSound == none || bSuppressSounds )
	{
		return false;
	}

	// limit the number of duplicate impact effects within a short period of time and close proximity
	if( (HitSound != MostRecentImpactSound) ||
		(VSizeSq(HitLocation - MostRecentImpactLocation) > MinRepeatImpactSoundDistanceSq) ||
		(`TimeSince(MostRecentImpactTime) > MinRepeatImpactSoundInterval) )
	{
		MostRecentImpactSound = AkEvent(HitSound);
		MostRecentImpactLocation = HitLocation;
		MostRecentImpactTime = WorldInfo.TimeSeconds;
		NumDuplicateImpactSounds = 0;

		return true;
	}

	if( NumDuplicateImpactSounds + 1 <= MaxDuplicateImpactSounds )
	{
		NumDuplicateImpactSounds += 1;
		return true;
	}

	return false;
}

/**
 * Play sound effect associated with this impact
 */
simulated function PlayImpactSound(Pawn EffectInstigator, vector HitLocation, vector FireDir, Actor HitActor, AkBaseSoundObject ImpactSound, KFImpactEffectInfo ImpactEffectInfo)
{
	local Vehicle V;

	V = Vehicle(HitActor);
	// if hit a vehicle controlled by the local player, always play it full volume
	if (V != None && V.IsLocallyControlled() && V.IsHumanControlled())
	{
		PlayerController(V.Controller).PlaySoundBase(ImpactSound, true);
	}
	else
	{
		PlaySoundBase(ImpactSound, true,,, HitLocation);
	}
}

simulated function CheckBulletWhip(vector FireDir, vector HitLocation, Pawn EffectInstigator, KFImpactEffectInfo ImpactEffectInfo)
{
	local KFPlayerController PC;

	ForEach LocalPlayerControllers(class'KFPlayerController', PC)
	{
		if ( !WorldInfo.GRI.OnSameTeam(Owner,PC) )
		{
			PC.CheckBulletWhip(ImpactEffectInfo.BulletWhipSnd, EffectInstigator.Location, FireDir, HitLocation, EffectInstigator);
		}
	}
}

/**
 * Play sound effect associated with this impact
 */
simulated function SpawnImpactDecal(vector HitLocation, vector HitNormal, Actor HitActor, const out MaterialImpactEffect ImpactEffect, optional TraceHitInfo HitInfo )
{
	local MaterialInterface MI;
	local MaterialInstanceTimeVarying MITV_Decal;
	local int DecalMaterialsLength;
	local float DecalSize, DecalThickness;

	// if we have a decal to spawn on impact
	DecalMaterialsLength = ImpactEffect.DecalMaterials.length;
	if( DecalMaterialsLength > 0 )
	{
		MI = ImpactEffect.DecalMaterials[Rand(DecalMaterialsLength)];
		if( MI != None )
		{
			DecalSize = RandRange(ImpactEffect.DecalMinSize, ImpactEffect.DecalMaxSize);			
			if ( ShouldExtendDecalThickness(HitActor) ) 
			{
				DecalThickness = DecalSize * 2.2f;
			}
			else
			{
				DecalThickness = 10.f;
			}

			if( MaterialInstanceTimeVarying(MI) != none )
			{
				// hack, since they don't show up on terrain anyway
				if ( Terrain(HitActor) == None )
				{
					MITV_Decal = new(self) class'MaterialInstanceTimeVarying';
					MITV_Decal.SetParent( MI );

					ImpactEffectDecalManager.SpawnDecal( MITV_Decal, HitLocation, rotator(-HitNormal), DecalSize,
						DecalSize, DecalThickness, false, (ImpactEffect.bNoDecalRotation) ? 0.f : (FRand() * 360.0), HitInfo.HitComponent, true, false, HitInfo.BoneName, HitInfo.Item, HitInfo.LevelIndex );
					//here we need to see if we are an MITV and then set the burn out times to occur
					MITV_Decal.SetScalarStartTime( ImpactEffect.DecalDissolveParamName, ImpactEffect.DecalDuration );
				}
			}
			else
			{
				ImpactEffectDecalManager.SpawnDecal( MI, HitLocation, rotator(-HitNormal), DecalSize,
					DecalSize, DecalThickness, false, (ImpactEffect.bNoDecalRotation) ? 0.f : (FRand() * 360.0), HitInfo.HitComponent, true, false, HitInfo.BoneName, HitInfo.Item, HitInfo.LevelIndex, ImpactEffect.DecalDuration );
			}
		}
	}
}

/**
 * On landscape with certain non-uniform scale (e.g. Zed Landing) decals have clipping
 * issues.  Increasing the thickness, and therefore bounds, resolves the issue at a
 * small perf cost.
 */
static function bool ShouldExtendDecalThickness(Actor HitActor)
{
	// using actor flags to reject most actors before casting		
	if ( HitActor != None && !HitActor.bGameRelevant && HitActor.bStatic && HitActor.IsA('Landscape') ) 
	{
		return true;
	}

	return false;
}

/**
 * Returns the impact sound that should be used for hits on the given physical material
 * UnrealScript returns structs by value, so use an out param instead
*/
static simulated function GetImpactEffect(PhysicalMaterial HitMaterial, out MaterialImpactEffect out_ImpactEffect, KFImpactEffectInfo ImpactEffectInfo )
{
	local int i;
	local KFPhysicalMaterialProperty PhysicalProperty;

	if (HitMaterial != None)
	{
		PhysicalProperty = KFPhysicalMaterialProperty(HitMaterial.GetPhysicalMaterialProperty(class'KFPhysicalMaterialProperty'));
	}

	// @todo: add alt fire
    if (ImpactEffectInfo != none)
    {
        if (PhysicalProperty != None)
        {
            i = ImpactEffectInfo.ImpactEffects.Find('MaterialType', PhysicalProperty.MaterialType);
            if (i != -1)
            {
                out_ImpactEffect = ImpactEffectInfo.ImpactEffects[i];
                return;
            }
        }

        out_ImpactEffect = ImpactEffectInfo.DefaultImpactEffect;
    }
}

simulated function bool AllowImpactEffects(Actor HitActor, vector HitLocation, vector HitNormal)
{
	return !WorldInfo.bDropDetail && (PortalTeleporter(HitActor) == None);
}

/*********************************************************************************************
 * Melee impacts
 *********************************************************************************************/

/**
 * Called by ProcessMeleeHit to spawn effects
 * Network: Local Player and Server
 */
simulated function PlayMeleeImpact(ImpactInfo Impact, Pawn EffectInstigator, KFWeapon InstigatorWeap, AkEvent HitWorldSound, AkEvent HitPawnSound, CameraShake Shake)
{
	// @todo: implement this using per material settings (see PlayImpactEffects)
}

defaultproperties
{
	DefaultImpactEffects=KFImpactEffectInfo'FX_Impacts_ARCH.Light_bullet_impact'

	MaxImpactEffectDistance=4000.0
	bAlignToSurfaceNormal=true
	MaxDecalRangeSQ=400000000.0

	// Actor
	bTickIsDisabled=true

	MinRepeatImpactSoundDistanceSq=10000 // 1m
	MinRepeatImpactSoundInterval=0
	MaxDuplicateImpactSounds=0
}
