//=============================================================================
// KFSpawner
//=============================================================================
// Placeable actor that can be used to script AI/Squad spawning
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Andrew "Strago" Ladenberger
//=============================================================================
class KFSpawner extends Actor
	native
	placeable;

/** Set to true if we're activating this from kismet. Set this to false if it is acting as a child spawner.
	This way any variables that the spawn will inherit from the parent will be greyed out */
var() bool	bIsTriggeredSpawner;

/** Largest type of squad that fits in this spawner */
var() ESquadType	LargestSquadType<EditCondition=bIsTriggeredSpawner>;
/** If > 0, automatically deactivate this spawner after some time */
var() float			MaxStayActiveTime<EditCondition=bIsTriggeredSpawner>;
/** Minimum time before getting re-activated */
var() float			CooldownTime<EditCondition=bIsTriggeredSpawner>;

/** Time between individual spawns within a squad */
var() float			SpawnInterval<EditCondition=bIsTriggeredSpawner>;

/** If set, yaw is randomized (useful for floor spawners) */
var() bool			bRandomizeSpawnYawRot;
/** The direction to spawn a zed if bRandomizeSpawnYawRot is true */
var transient int	LastSpawnYawRot;

/** If set, this spawner will auto-deactivate if this actor (e.g. Trigger, TriggerVolume) is not touching any players */
var() Actor			ActorToReacquirePlayerTouch<EditCondition=bIsTriggeredSpawner>;
/** If > 0, players must be touching the trigger for some amount of time before it can be used */
var() float			MinReacquireTouchTime<EditCondition=bIsTriggeredSpawner>;
var transient float	ReacquireTouchTimeLeft;

/** Number of zeds required to be touching the ActorToReacquirePlayerTouch for this spawning to be active */
var() int			NumTouchingZedsRequired<EditCondition=bIsTriggeredSpawner>;
/** Are enough zeds touching this actor to be able to spawn zeds here */
var transient bool  bRequiredZedAmountTouching;

/** Additional spawn points for pending zeds when this spawner is active */
var() array<KFSpawner> ChildSpawners;

/** list of actor classes waiting to spawn */
var transient array< class<KFPawn_Monster> > PendingSpawns;

/** The animation direction of the last wall spawned zed */
var transient byte	LastAnimDirection;

/** Local timers for CanSpawnHere */
var protected transient float LastActivationTime;
var protected transient float LastSpawnTime;

/** Is this spawner active? */
var bool bIsActive;
/** Is this spawner currently in the process of spawning? */
var bool bIsSpawning;

/** Will log out/display spawning info for this spawn volume */
var bool bDebugSpawning;

/*********************************************************************************************
 `* Effects
********************************************************************************************* */

enum EEmergeAnim
{
	EMERGE_Floor,
	EMERGE_Wall248UU,
	EMERGE_WallHigh,
	EMERGE_Ceiling,
	EMERGE_None,
};

/** Animation to play when exiting the spawner */
var() EEmergeAnim	EmergeAnim;

/** Reference to destructible object that we want to smash through */
var() KFDestructibleActor DestructibleToBreak;

/*********************************************************************************************
 `* Debugging
********************************************************************************************* */

/** If set, do not activate child spawners */
var transient bool bIgnoreChildren;

cpptext
{
	/** Warn the mapper if a child KFSpawner has a "LargestSquadType" that's smaller than it's parent */
#if WITH_EDITOR
	virtual void CheckForErrors();
#endif
	/** Update SpawnInterval in tick */
	virtual void TickSpecial(FLOAT DeltaTime);

	/** Spawns an AI from the pending spawns list */
	UBOOL		SpawnAI(UClass* ActorClass);
	/** If we have child spawners, randomely select a spawn location the list */
	AKFSpawner* ChooseChildSpawner();

	/** Gets a random direction to spawn our zed that's different than the last */
	INT GetRandomYawRot();
}

function OnToggle( SeqAct_Toggle Action )
{
	// On
	if( Action.InputLinks[0].bHasImpulse || (Action.InputLinks[2].bHasImpulse && !bIsActive) )
	{
		ActivateSpawner();
	}
	// Off
	else
	{
		DeactivateSpawner();
	}
}

function ActivateSpawner()
{
	local KFGameInfo KFGI;

	if ( !bIsActive )
	{
		// restart reaquire touching actors interval
		if ( MinReacquireTouchTime > 0 && ActorToReacquirePlayerTouch != None )
		{
			ReacquireTouchTimeLeft = MinReacquireTouchTime;
			SetTimer(1.0, true, nameof(ReacquirePlayerTouch));
		}

		// restart reaquire touching actors interval
		if ( NumTouchingZedsRequired > 0 && ActorToReacquirePlayerTouch != None )
		{
			SetTimer(1.0, true, nameof(ReacquireZedTouch));
		}
	}

	bIsActive = true;
	LastActivationTime = WorldInfo.TimeSeconds;

	// register with the SpawnManager
	KFGI = KFGameInfo( WorldInfo.Game );
	if ( KFGI != None && KFGI.SpawnManager != None )
	{
		KFGI.SpawnManager.ActiveSpawner = self;
	}
}

function DeactivateSpawner()
{
	bIsActive = false;
	LastActivationTime = -1.f;

	ClearTimer(nameof(ReacquirePlayerTouch));
	ClearTimer(nameof(ReacquireZedTouch));
}

function bool CanSpawnHere(ESquadType DesiredSquadType)
{
	// check if activated
	if ( !bIsActive )
		return false;

	// check valid squad type
	if ( DesiredSquadType < LargestSquadType )
		return false;

	// check MaxStayActiveTime
	if ( MaxStayActiveTime > 0 && `TimeSince(LastActivationTime) > MaxStayActiveTime )
		return false;

	// check CooldownTime
	if ( CooldownTime > 0 && LastSpawnTime > 0 && `TimeSince(LastSpawnTime) < CooldownTime )
		return false;

	// check if player touch is still being acquired
	if ( ReacquireTouchTimeLeft > 0.f )
		return false;

	// check if already spawning
	if ( PendingSpawns.Length > 0 )
		return false;

    // Not enough touching zeds here!
    if( ActorToReacquirePlayerTouch != none && NumTouchingZedsRequired > 0
        && bRequiredZedAmountTouching == false )
    {
        return false;
    }

	return true;
}

/** Check if any players are still in the instigating trigger/triggervolume */
function ReacquirePlayerTouch()
{
	local Pawn P;
	local bool bHasAlivePlayers;

	ForEach ActorToReacquirePlayerTouch.TouchingActors(class'Pawn', P)
	{
		if ( P.Controller != None && P.IsAliveAndWell() )
		{
			// Human Team
            if( P.GetTeamNum() == 0 )
			{
                bHasAlivePlayers = true;
            }
		}
	}

	// auto-deactivate if there are no living players in the area
	if ( !bHasAlivePlayers )
	{
		`log(self@"ReacquirePlayerTouch is auto-deactivating this spawner", bDebugSpawning);

		ClearTimer(nameof(ReacquirePlayerTouch));
		DeactivateSpawner();
	}

	ReacquireTouchTimeLeft -= 1.f;
	if ( ReacquireTouchTimeLeft <= 0.f )
	{
		`log(self@"ReacquirePlayerTouch completed successfully", bDebugSpawning);

		ClearTimer(nameof(ReacquirePlayerTouch));
	}
}

/** Check if any zeds are still in the instigating trigger/triggervolume */
function ReacquireZedTouch()
{
	local Pawn P;
	local int NumAliveZedsTouching;

    if( !bIsActive )
    {
        return;
    }

	ForEach ActorToReacquirePlayerTouch.TouchingActors(class'Pawn', P)
	{
		if ( P.Controller != None && P.IsAliveAndWell() )
		{
            // Zed Team
            if( P.GetTeamNum() != 0 )
			{
                NumAliveZedsTouching++;
            }
		}
	}

	`log("NumAliveZedsTouching = "$NumAliveZedsTouching, bDebugSpawning);

    // See if enough zeds are touching to set the flag to true
    if( NumAliveZedsTouching >= NumTouchingZedsRequired )
    {
        bRequiredZedAmountTouching = true;
    }
    else
    {
        bRequiredZedAmountTouching = false;
    }
}


/** Initiate spawning.  Returns a list of classes that weren't able to be spawned here */
function int SpawnSquad( out array< class<KFPawn_Monster> > SpawnList )
{
	if ( SpawnList.Length == 0 )
		return 0;

	AddPendingSpawns( SpawnList );

	LastSpawnTime = WorldInfo.TimeSeconds;
	bIsSpawning = PendingSpawns.Length > 0;

	return PendingSpawns.Length;
}

/** Validate and copy a list of classes to spawn into our PendingSpawn list */
function AddPendingSpawns( out array< class<KFPawn_Monster> > SpawnList )
{
	local int i;
	local KFSpecialMoveHandler SMH;

	// if we don't need an animation, skip the SpecialMove (only valid if we're not using child spawners)
	if ( EmergeAnim == EMERGE_None && ChildSpawners.Length == 0 )
	{
		PendingSpawns = SpawnList;
		SpawnList.Length = 0;
		return;
	}

	// Remove all monsters from the list that don't have emerge moves
	for ( i = SpawnList.length - 1; i >= 0 ; i-- )
	{
		SMH = SpawnList[i].default.SpecialMoveHandler;
		if ( ESpecialMove(SM_Emerge) < SMH.SpecialMoveClasses.Length && SMH.SpecialMoveClasses[SM_Emerge] != None )
		{
			PendingSpawns.AddItem( SpawnList[i] );
			SpawnList.Remove( i, 1 );
		}
	}
}

/**
 * Called when a new pawn is spawned using this spawner,
 * use to handle any special behaviors, such as playing
 * an animation, etc.
 */
event HandleSpawn(KFPawn NewSpawn, int SlotIdx)
{
	local KFGameInfo KFGI;
`if(`notdefined(ShippingPC))
	local KFGameReplicationInfo KFGRI;
`endif

	// instantly damage our linked destructible
	if ( DestructibleToBreak != None && !DestructibleToBreak.bShutDown )
	{
		// use RadiusDamage so that we don't need a valid hit component
		DestructibleToBreak.TakeRadiusDamage(NewSpawn.Controller, 10000, 0, class'DmgType_Crushed', 0, Location, true, self);
	}

	// Handle emerge move/animation
	if ( EmergeAnim != EMERGE_None )
	{
		NewSpawn.DoSpecialMove( SM_Emerge,,, class'KFSM_Emerge'.static.PackAnimFlag( EmergeAnim, LastAnimDirection ) );

		if ( !NewSpawn.IsDoingSpecialMove(SM_Emerge) )
		{
			`warn("SM_Emerge failed for"@NewSpawn);
			NewSpawn.Died( None, WorldInfo.KillZDamageType, Location );
			HandleFailedSpawn();
		}
	}

`if(`notdefined(ShippingPC))
    // Let the GRI know that a spawn volume was just used
	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
	if( KFGRI != none && KFGRI.bTrackingMapEnabled )
	{
		KFGRI.AddRecentSpawnVolume(Location, true);
	}
`endif

	// Recount number of living AI for wave gametype
	KFGI = KFGameInfo( WorldInfo.Game );
	if ( KFGI != None )
	{
		KFGI.RefreshMonsterAliveCount();
	}
}

/**
 * Called when the spawner fails to spawn a pawn
 */
event HandleFailedSpawn()
{
	local KFGameInfo KFGI;
`if(`notdefined(ShippingPC))
	local KFGameReplicationInfo KFGRI;
`endif

    `warn(self@PendingSpawns.Length$" zeds failed to spawn at this portal spawn!!!");

    // Removed failed spawns from the NumAISpawnsQueued;
	KFGI = KFGameInfo( WorldInfo.Game );
	if ( KFGI != None )
	{
		`log(self@GetFuncName()$" removing "$PendingSpawns.Length$" from NumAISpawnsQueued due to failed spawn. NumAISpawnsQueued: "$KFGI.NumAISpawnsQueued, bDebugSpawning);
        KFGI.NumAISpawnsQueued -= PendingSpawns.Length;
		KFGI.NumAIFinishedSpawning -= PendingSpawns.Length;
	}

`if(`notdefined(ShippingPC))
    	// Let the GRI know that a portal spawn failed to spawn some AI
    	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
    	if( KFGRI != none && KFGRI.bTrackingMapEnabled )
    	{
    		KFGRI.AddFailedSpawn(Location, true);
    	}
`endif

    if( bDebugSpawning )
    {
    	if ( KFGI != None && KFGI.SpawnManager != None )
    	{
    		KFGI.SpawnManager.ActiveSpawner = self;
    		if( bDebugSpawning )
    		{
                KFGI.SpawnManager.LogMonsterList(PendingSpawns, "Failed Pending Spawns For "$Self);
            }
    	}
    }

    // If a zed failed to spawn, clear the list
    PendingSpawns.Length = 0;
}

/** Debugging */
function bool TestSpawn(class<KFPawn_Monster> SpawnClass, optional int NumSpawns=1, optional bool bImmediate)
{
	local array< class<KFPawn_Monster> > TestSpawnList;

	while( NumSpawns > 0 )
	{
		TestSpawnList.AddItem(SpawnClass);
		NumSpawns--;
	}

	SpawnSquad(TestSpawnList);

	if ( bImmediate )
	{
		// don't wait for spawn interval, update spawn next tick
		LastSpawnTime -= SpawnInterval;
	}

	return bIsSpawning;
}

DefaultProperties
{
	// Ensures we can randomize in all directions
	bIsTriggeredSpawner=true
	LastAnimDirection=255
	LastSpawnYawRot=-1

	SpawnInterval=0.5
	CooldownTime=20.f
	LargestSquadType=EST_Medium

	Begin Object Class=StaticMeshComponent Name=PreviewMesh
		HiddenGame=TRUE
		StaticMesh=StaticMesh'ZED_Clot_MESH.ZED_Clot'
		Rotation=(Yaw=-16384)
	End Object
	Components.Add(PreviewMesh)
}
