/*
	--------------------------------------------------------------
	KF_Wave_Controller
	--------------------------------------------------------------

    This actor does all the dirty work for the Wave Designer. It
    handles Timing for wave spawns, tracks the number of monsters
    in each wave and the wave spawn cycle.

    The Progression of a Wave goes something like :

    New Wave -->   New Cycle --->  Pick a Squad ---> Spawn Squad

    this repeats until the total number of ZEDs is equal to the
    Max monsters value set in the cycle.  Then we move on to the
    next cycle and repeat.

    It's worth noting that 'standard' KF  Waves would only have a
    single Spawn cycle per wave. Multiple cycles are only really
    needed for setting up complex waves where you want lots of
    variation in the frequency or type of monsters being spawned.

	Author :  Alex Quick

	--------------------------------------------------------------
*/

class KF_Wave_Controller extends Actor
dependsOn(KFStoryGameInfo);

/*  we've spawned everything we can possibly spawn.  Tell the next controller to start doing its thing .. */
var         bool                                        bSpawningComplete;

var         KFStoryGameInfo                             StoryGI;

var array < class<KFMonster> >                          NextSpawnSquad;

/*===========================================================================================================
Wave Vars
============================================================================================================*/

/* array of all Zombie Volumes which will spawn ZEDs for this wave */
var         array<ZombieVolume>                         WaveVols;

/* Set by the Designer - reference to tag of volumes used in this wave */
var         array<name>                                 Wave_VolumeTags;

/* Total number of ZEDs which will be spawned for this wave */
var         int                                         Wave_ZEDTotal;

/* Total number of ZEDs remaining in this wave (sum of the total spawn count from all cycles)   */
var         int                                         Wave_RemainingZEDs;

/* Reference to the index of this Controller in it's owning Designer's Wave array */
var         int                                         WaveIndex;

/* number of ZEDs which were spawned by this Controller in the current wave */
var         int                                         RunningSpawnTotal;

/* Number of ZEDs left in the map after this wave ended */
var         int                                         NumStragglers;

/*==========================================================================================================
Spawn Cycle Vars
============================================================================================================*/

/*  index of the current SpawnGroup in this wave */
var         int                                         CurrentCycleIdx;

var         KFStoryGameInfo.SZEDSquadType               CurrentSquad;

var         array<KF_StoryWaveDesigner.SWaveSpawn>      AllSpawnCycles;

var         KF_StoryWaveDesigner.SWaveSpawn             CurrentSpawnCycle;

/* Wave Designer actor which this Controller belongs to */
var         private KF_StoryWaveDesigner                OwningManager;

/* Should we be spitting out ZEDs ? */
var         bool                                        bActive;

/* number of Spawn Cycles left to spawn for this wave */
var         int                                         RemainingSpawnCycles;

/* Number of ZEDs left in the current spawn cycle */
var         int                                         CycleRemainingZEDs;

var         int                                         Cycle_ZEDTotal;

var         ZombieVolume                                LastSpawningVolume;

var         int                                         NumFailedFindVolumeAttempts,MaxFailedFindVolumeAttempts;
var         int                                         NumFailedSquadSpawnAttempts,MaxFailedSquadSpawnAttempts;

var         float                                       WaveTimeElapsed, LastWaveSpawnTime;

var         KF_StoryWaveDesigner.ESineWavePatternUsage  CurrentSineWavePatternUsage;

/* We have finished spawning all the zeds in the current Squad we're trying to spawn */
var         bool                                        bFinishedLastSquadSpawn;

/* The name of the last squad we tried to spawn */
var         string                                      LastSpawnSquadName;


/*============================================================================================================= */

function Initialize()
{
	StoryGI = KFStoryGameInfo(level.Game);
    Tag = GetOwningManager().Waves[WaveIndex].Wave_ActivationTag ;

    /* Figure out which volumes will be used for s pawning in this wave */
    Wave_VolumeTags = GetOwningManager().Waves[WaveIndex].Wave_VolumeTags ;
    FindControlledVolumes();
}


/* Figure out which volumes will be used to spawn ZEDs for this wave */
function FindControlledVolumes()
{
    local ZombieVolume V;
    local int i;

    foreach AllActors(class 'ZombieVolume' , V)
    {
        for(i=0; i < Wave_VolumeTags.length ; i ++)
        {
            if(V.Tag == Wave_VolumeTags[i])
            {
                V.SetOwner(OwningManager);
                WaveVols[WaveVols.length] = V;
            }
        }
    }
}

function Reset()
{
    bSpawningComplete = false;
    bActive = false;
    Wave_ZEDTotal = 0;
    Cycle_ZEDTotal = 0;
    CycleRemainingZEDS = 0;
    Wave_RemainingZEDs = 0;
    CurrentCycleIdx = default.CurrentCycleIdx;
    RemainingSpawnCycles = 0;
    RunningSpawnTotal = 0;
    NumStragglers=0;
}


function SetOwningManager()
{
    OwningManager     =  KF_StoryWaveDesigner(Owner);
}

function KF_StoryWaveDesigner   GetOwningManager()
{
    return OwningManager;
}

function InitSpawns()
{
    local int i,idx;
    local int NewTotalZEDs;

    AllSpawnCycles          = GetOwningManager().Waves[WaveIndex].Wave_Spawns;
    RemainingSpawnCycles    = AllSpawnCycles.length;
    LastWaveSpawnTime = 0;
    CurrentSineWavePatternUsage = GetOwningManager().Waves[WaveIndex].SineWavePatternUsage;
    bFinishedLastSquadSpawn=true;

    // Scale the WaveTimeElapsed to modify where we start in the sine wave pattern
    if( CurrentSineWavePatternUsage == SWP_StartAtMinRate || CurrentSineWavePatternUsage == SWP_AlwaysFullSpeed )
    {
        WaveTimeElapsed = 0;
    }
    else if( CurrentSineWavePatternUsage == SWP_StartAtMaxRate )
    {
        WaveTimeElapsed = 40;
    }

    for(i = 0 ; i < RemainingSpawnCycles ; i ++)
    {
        for(idx = 0 ; idx < AllSpawnCycles[i].SquadList.length ; idx ++)
        {
            AllSpawnCycles[i].AllSquads[idx] = StoryGI.FindSquadByName(AllSpawnCycles[i].SquadList[idx]);
        }

        NewTotalZEDs += GetCycleMaxZEDs(i);
    }

    Wave_ZEDTotal      = NewTotalZEDs;
    Wave_RemainingZEDs = Wave_ZEDTotal;

    if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
    {
        log("Init spawns for wave : "@WaveIndex@" - Total Enemies for this wave will be : "@Wave_ZEDTotal,'Story_Debug');
    }
}


/* Updates Zombie Volume Events & Tags to match the settings for the current spawn cycle */
function UpdateVolumeTags(ZombieVolume V)
{
    V.ZombieDeathEvent = CurrentSpawnCycle.ZombieDeathEvent;
    V.ZombieSpawnTag   = CurrentSpawnCycle.ZombieSpawnTag;
    V.ZombieSpawnEvent = CurrentSpawnCycle.ZombieSpawnEvent;
}

/* Returns true if this wave spawns endless enemies */
function bool InfiniteSpawns()
{
    return GetMaxMonsters() == 0 || GetCycleMaxZEDs(CurrentCycleIdx) == 0 ;
}


function AllSpawningFinished()
{
    bSpawningComplete = true;

    if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
    {
        log("=========================================================",'Story_Debug');
        log(self@"  - Wave : "@WaveIndex@" has spawned all ("@RunningSpawnTotal@") its ZEDs. ",'Story_Debug');
    }

    NumStragglers = StoryGi.NumMonsters;
    //log("Number of stragglers at the time this Wave ended was : "@NumStragglers,'Story_Debug');
}


function bool IsDirectingSpawnsFor( ZombieVolume TestVol)
{
    local int i;

    for(i = 0 ; i < WaveVols.length ; i ++)
    {
       if(WaveVols[i] == TestVol)
       {
           return true;
       }
    }

    return false;
}


/* Triggering an active wave aborts it.  Triggering an inactive wave starts it up */
function Trigger( actor Other, pawn EventInstigator )
{
   if(!bActive)
   {
      StartWave(Other);
   }
   else
   {
      AbortWave(Other);
   }
}


function StartWave(optional Actor TriggeringActor)
{
    if(bSpawningComplete)
    {
        Reset();
    }

    if(!bSpawningComplete)
    {
        bActive = true;

        if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
        {
            log("======================================================================",'Story_Debug');
            log("Starting  :"@Tag@" for Wave Designer : "@GetOwningManager()@" - Instigator : "@TriggeringActor,'Story_Debug');
        }

        /* Set up the Spawn Cycles & Calculate the ZED totals */
        InitSpawns();
        StartNextSpawnCycle();
        GetOwningManager().GoToWave(tag);

        SetTimer(GetSpawninterval(),false);
        DoSquadSpawn();
    }
}
function AbortWave(optional Actor TriggeringActor)
{
    bActive = false;

    if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
    {
        log("======================================================================",'Story_Debug');
        log("Stopping :"@Tag@" for Wave Designer : "@GetOwningManager()@" - Instigator : "@TriggeringActor,'Story_Debug');
    }

   /* We're trying to end this wave but the spawning hasn't yet finished. Cut it off */
    AllSpawningFinished();

/*    if(GetOwningManager().Waves[WaveIndex].bKillStragglers)
    {
        KillStragglers();
    }
*/
}

/* Timed ZED spawning*/
function Timer()
{
    if(!bSpawningComplete && bActive)
    {
        // Increment the wave time counter
        if( LastWaveSpawnTime > 0 )
        {
            WaveTimeElapsed += (Level.TimeSeconds - LastWaveSpawnTime);
        }

        DoSquadSpawn();
        SetTimer(GetSpawninterval(),false);
    }
}

/* We're moving to another wave - Kill off the remaining zeds as elegantly as possible ... */
function KillStragglers()
{
    local int i;
    local int idx;
    local KFMonster Straggler;
    local array<KFMonster>  MarkedForDeath;

    // - need to set this before we start killing zeds off or the array size will change while we're iterating it ...
    for(i = 0 ; i < WaveVols.length ; i ++)
    {
        for(idx = 0 ; idx < WaveVols[i].ZEDList.length ; idx ++)
        {
            Straggler = WaveVols[i].ZEDList[idx] ;
            if(Straggler != none && Straggler.Event == CurrentSpawnCycle.ZombieDeathEvent &&
             !IsVisibleToAnyone(Straggler) )
            {
                Straggler.Event = '';
                MarkedForDeath[MarkedForDeath.length] = Straggler;
            }
        }
    }

    i= 0;
    for(i = 0 ; i < MarkedForDeath.length ; i ++)
    {
        Straggler = MarkedForDeath[i];
        if(Straggler != none)
        {
            Straggler.Died(Straggler.Controller, class'Gibbed', Straggler.Location );
        }
    }
}

/* Returns true if the supplied actor is visible to any player on the server */
function   bool IsVisibleToAnyone(Pawn A)
{
    local Controller C;
    local KFPlayerController_Story PC;
    local float MinKillDistSq;

    MinKillDistSq = 1000 * 1000;

	for( C=Level.ControllerList; C!=None; C=C.NextController )
	{
        PC = KFPlayerController_Story(C);
        if(PC != none)
        {
            if(PC.HasLineOfSightTo(A.Location + (vect(0,0,1) * (A.CollisionHeight/2))) &&
            VSizeSquared(A.Location - PC.CalcViewLocation) < MinKillDistSq)
            {
                return true;
            }
        }
	}

    return false;
}

/* Returns true if the number of monsters currently in play exceeds the total limit as defined in the Level Rules.
This is primarily for performance reasons ... */

function bool  MaxMonstersAtOnce(int NewMonsters, optional out Int ClampedNewMonsters)
{
    local bool bReachedMax;

    if(StoryGI != none)
    {
        bReachedMax = Max(StoryGI.NumMonsters,0) + NewMonsters > StoryGI.StoryRules.MaxEnemiesAtOnce ;

        if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
        {
            log("Current Monsters : "@Max(StoryGI.NumMonsters,0)@"Wave Max : "@GetMaxMonsters()@" Level Max : "@StoryGI.StoryRules.MaxEnemiesAtOnce,'Story_Debug');
        }

        if(bReachedMax)
        {
            ClampedNewMonsters = Min(NewMonsters,StoryGI.StoryRules.MaxEnemiesAtOnce - Max(StoryGI.NumMonsters,0));
            return true ;
        }
    }

    return false;
}

/* Builds a squad and passes it to SpawnASquad() */
function DoSquadSpawn()
{
    local int NumToSpawn,ClampedMonsters;
    local int IgnoreZEDLimit;
    local string SquadName;
    local int I;

    if(bSpawningComplete)
    {
        return;
    }

    LastWaveSpawnTime = Level.TimeSeconds;

    if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
    {
        log("**** DO SQUAD SPAWN *******************************************************",'Story_Debug');
    }

    if( NextSpawnSquad.length > 0 && !bFinishedLastSquadSpawn )
    {
        // NextSpawnSquad needs to finish spawning the last spawn squad we tried!
        SquadName = LastSpawnSquadName;
    }
    else
    {
        NextSpawnSquad = BuildNextSpawnSquad(IgnoreZedLimit,SquadName);
        LastSpawnSquadName = SquadName;
        bFinishedLastSquadSpawn=false;
    }
    NumToSpawn = NextSpawnSquad.length;

    /* Make sure the squad size never exceeds the number of guys remaining to spawn */
    if(!InfiniteSpawns())
    {
        NumToSpawn = Min(NumToSpawn, GetRemainingMonstersInCycle());
    }

    /* Make sure the squad size never exceeds the total max enemies value in the level rules */
    if(IgnoreZEDLimit == 0 && MaxMonstersAtOnce(NumToSpawn,ClampedMonsters))
    {
        //log("DoSquadSpawn want to spawn "$NumToSpawn$" can only spawn "$ClampedMonsters,'Story_Debug');
        for(i=0; i < NextSpawnSquad.length ; i ++)
        {
            if( i < ClampedMonsters )
            {
                //log("DoSquadSpawn wants to spawn "$NextSpawnSquad[i],'Story_Debug');
            }
            else
            {
                //log("DoSquadSpawn wants to spawn "$NextSpawnSquad[i]$" but can't because there are too many monsters already",'Story_Debug');
            }
        }
        NumToSpawn = ClampedMonsters;
    }
    else
    {
        for(i=0; i < NextSpawnSquad.length ; i ++)
        {
            //log("DoSquadSpawn wants to spawn "$NextSpawnSquad[i],'Story_Debug');
        }
    }

    if(NumToSpawn > 0)
    {
       SpawnASquad(NumToSpawn,SquadName);
    }
}

function bool SpawnASquad(int NumDesired, optional string SquadName)
{
    local ZombieVolume   SpawnVol;
    local int NumSpawned;
    local int Diff;
    local int i;


    // Log what the squad is going to be when we spawn it

    for(i=0; i < NextSpawnSquad.length ; i ++)
    {
        //log("SpawnASquad wants to spawn "$NextSpawnSquad[i],'Story_Debug');
    }

    StoryGI.NextSpawnSquad = NextSpawnSquad;         // needs to be set *before* we look for a Zombie Volume, as RateZombieVolume() checks this array

    SpawnVol = GetASpawnVolume();
    if(SpawnVol != none )
    {
        NumFailedFindVolumeAttempts = 0;
        LastSpawningVolume = SpawnVol ;
        UpdateVolumeTags(SpawnVol);

        /* Perform the actual spawn */
        StoryGI.SpawnZEDsInVolume(SpawnVol,NumDesired,NumSpawned,true);

        if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
        {
            log("*** Spawned : "@NumSpawned@" ZEDs for Squad : "@SquadName,'Story_Debug');
        }

        RunningSpawnTotal += NumSpawned ;
        UpdateRemainingZEDs(NumSpawned);

        Diff = Max(NumDesired - NumSpawned,0);

        if(Diff == 0 )
        {
            // Only say the squad spawn is complete if all zeds from the squad have spawned
            if( StoryGI.NextSpawnSquad.length == 0 )
            {
                OnSquadSpawnComplete(SquadName);
            }
            else
            {
                // Make sure you use the squad from the Story GI before you recurse
                // as it has had the zeds you already spawned removed
                NextSpawnSquad  = StoryGI.NextSpawnSquad;
            }
            NumFailedFindVolumeAttempts = 0;
            return true;
        }
        else
        {
            if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
            {
                log("Wanted to spawn"@NumDesired@" zeds, but could only spawn : "@NumSpawned,'Story_Debug');
                log(" Trying again with : "@Diff@" Zeds ... ", 'Story_Debug');
            }

            // Make sure you use the squad from the Story GI before you recurse
            // as it has had the zeds you already spawned removed
            NextSpawnSquad  = StoryGI.NextSpawnSquad;

            // Don't let this infinitely recurse and crash the game, give up
            // if it can't spawn.
            if(NumFailedSquadSpawnAttempts < MaxFailedSquadSpawnAttempts)
            {
                SpawnASquad(Diff,SquadName);  // recurse.
            }
            else
            {
                if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
                {
                    Log(" *** CRITICAL SPAWNING FAILURE.   could not spawn : "@SquadName@"in,  after : "@NumFailedSquadSpawnAttempts@" attempts!",'Story_Debug');
                }
            }
        }
    }
    else
    {
        NumFailedFindVolumeAttempts ++;
        if(NumFailedFindVolumeAttempts < MaxFailedFindVolumeAttempts)
        {
            SpawnASquad(NumDesired,SquadName);  // recurse.
        }
        else
        {
            if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
            {
                Log(" *** CRITICAL SPAWNING FAILURE.   could not find a volume to spawn : "@SquadName@"in,  after : "@NumFailedFindVolumeAttempts@" attempts!",'Story_Debug');
            }
        }
    }

    return false;
}

function UpdateRemainingZEDs(int NumSpawned)
{
    CycleRemainingZEDs = Max(CycleRemainingZEDs - NumSpawned,0);
    Wave_RemainingZEDs = Max(Wave_RemainingZEDs - NumSpawned,0);
}

/* A ZED Squad was just spawned successfully . Increment the appropriate values
to keep the spawn cycle moving */

function OnSquadSpawnComplete(optional string SquadName)
{
    local int SquadIdx;

    //log(SquadName@" just spawned successfully!", 'Story_Debug');

    bFinishedLastSquadSpawn=true;

    if(SquadName != "")
    {
        FindSquadByName(SquadName,SquadIdx);
        CurrentSpawnCycle.AllSquads[SquadIdx].LastSquadSpawnTime = Level.TimeSeconds;
    }

    if(GetRemainingMonstersInCycle() == 0 )
    {
        StartNextSpawnCycle();
    }
}

function StartNextSpawnCycle()
{
    local int NumSpawnCycles;

    NumSpawnCycles = AllSpawnCycles.length ;
    if(RemainingSpawnCycles > 0 )
    {
        CurrentCycleIdx                 = Min(CurrentCycleIdx + 1 , NumSpawnCycles-1);
        CurrentSpawnCycle               = AllSpawnCycles[CurrentCycleIdx] ;
        RemainingSpawnCycles            = Max(RemainingSpawnCycles - 1,0) ;
        Cycle_ZEDTotal                  = GetCycleMaxZEDs(CurrentCycleIdx);
        CycleRemainingZEDs              = Cycle_ZEDTotal;
        CurrentSpawnCycle.SpawnedSquads.Length = 0;
    }
    else
    {
        if(GetRemainingMonsters() == 0 &&
        !InfiniteSpawns() )
        {
            AllSpawningFinished();
        }
    }
}

/* Find a volume to spawn the next squad of ZEDs in */
function ZombieVolume   GetASpawnVolume()
{
    local ZombieVolume  SpawnVol;
    local bool bSpawningPatriarch;

    bSpawningPatriarch     = SquadContainsZED(NextSpawnSquad,class 'ZombieBoss');
    SpawnVol               = RateVolumes(,bSpawningPatriarch);

    if(SpawnVol == none)
    {
        if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
        {
            log("== WARNING : "@self@" could not find a suitable spawn volume for : "@CurrentSquad.Squad_Name,'Story_Debug');
        }
    }

    return SpawnVol ;
}


function ZombieVolume RateVolumes(optional bool bIgnoreFailedSpawnTime, optional bool bBossSpawning)
{
	local ZombieVolume BestZ;
	local float BestScore,tScore;
	local int i,l;
	local Controller C;
	local array<Controller> CL;
	local int NumInvalidvols;

	// First pass, pick a random player.
	for( C=Level.ControllerList; C!=None; C=C.NextController )
	{
		if( C.bIsPlayer && C.Pawn!=None && C.Pawn.Health>0 )
			CL[CL.Length] = C;
	}
	if( CL.Length>0 )
		C = CL[Rand(CL.Length)];
	else if( C==None )
		return None; // Shouldnt get to this case, but just to be sure...

	// Second pass, figure out best spawning point.
	l = WaveVols.length ;
//	log(self@" RateVolumes :: Number of volumes to choose from : "@l);
	if(l == 0)
	{
        if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
        {
	       log(" WARNING -  NO ZOMBIE VOLUMES ASSOCIATED WITH WAVE : "@WaveIndex@" OF :"@GetOwningManager(),'Story_Debug');
        }

        return none;
	}
	for( i=0; i<l; i++ )
	{
        tScore = WaveVols[i].RateZombieVolume(StoryGI,LastSpawningVolume,C,bIgnoreFailedSpawnTime, bBossSpawning);

		if( tScore<0 )
		{
            NumInvalidVols ++;
			continue;
		}

        if( BestZ==None || (tScore>BestScore) )
		{
			BestScore = tScore;
			BestZ = WaveVols[i];
		}
	}

//	log(self@" RateVolumes :: Number of invalid volumes : "@NumInvalidVols);


	return BestZ;
}

/* Returns true if the supplied monster squad has a monster of the specified type in it */
function bool SquadContainsZED(array < class<KFMonster> > CheckSquad, class <KFMonster> ClassType)
{
    local int i;

    for(i = 0 ; i < CheckSquad.length ; i ++)
    {
       if(ClassisChildOf(CheckSquad[i],ClassType))
       {
           return true;
       }
    }

    return false;
}

/* looks up a squad of the supplied name in the Current Spawn Cycle's cached Squads list */

function KFStoryGameInfo.SZEDSquadType     FindSquadByName(string SquadName, optional out int Index)
{
    local int i;
    local KFStoryGameInfo.SZEDSquadType ExportSquad;

    for(i = 0 ; i < CurrentSpawnCycle.AllSquads.length ; i ++)
    {
        if(CurrentSpawnCycle.AllSquads[i].Squad_Name == SquadName)
        {
            Index = i;
            ExportSquad = CurrentSpawnCycle.AllSquads[i] ;
            break;
        }
    }

    if(ExportSquad.Squad_Name == "")
    {
       //log("Warning - could not find a suitable struct for Squad of Name : "@SquadName);
    }

    return ExportSquad;
}


function float GetSpawnInterval()
{
    local float AdjustedInterval;
    local bool bFullSpeedSpawning;

    // Set full speed spawning if we want that
    if( CurrentSineWavePatternUsage == SWP_AlwaysFullSpeed )
    {
        bFullSpeedSpawning = true;
    }

    if(StoryGI != none)
    {
        AdjustedInterval = StoryGI.GetAdjustedSpawnInterval(CurrentSpawnCycle.SpawnInterval, WaveTimeElapsed, bFullSpeedSpawning) ;
    }

    if(GetOwningManager() != none &&
    GetOwningManager().bPrintDebugLogs)
    {
    	log(tag@"--> Next ZED Spawn will occur in : "@AdjustedInterval@" Seconds ... ",'Story_Debug');
    }

    return AdjustedInterval;
}

/* Returns the total number of ZEDs that will spawn in this wave*/
function   int GetMaxMonsters()
{
    return Wave_ZEDTotal;
}


/* Returns the total number of ZEDs to spawn in a given cycle,  taking into account player & game difficulty scaling */
function int GetCycleMaxZEDs(int CycleIndex)
{
    local int AdjustedMaxZEDs;

    AdjustedMaxZEDs = AllSpawnCycles[CycleIndex].MaxZEDs;

    /* Infinite spawns, dont bother applying difficulty scaling */
    if(AdjustedMaxZEDs == 0)
    {
        return 0;
    }

    if(!AllSpawnCycles[CycleIndex].bNoDifficultyScaling)
    {
        AdjustedMaxZEDs *= StoryGI.GetZEDCountModifier() ;
    }

    AdjustedMaxZEDs = Max(AdjustedMaxZEDs,1);

    return AdjustedMaxZEDs;
}

function   int GetRemainingMonsters()
{
    return Wave_RemainingZEDs;
}

function   int GetRemainingMonstersInCycle()
{
    return CycleRemainingZEDs;
}

/* Since the priority enums aren't ordered from low to high ... */
function    int GetOrderedPriority(int PriorityVal)
{
    switch(PriorityVal)
    {
        case 0 :  return 2;     // Normal
        case 1 :  return 1;     // Low
        case 2 :  return 0;     // VeryLow
        case 3 :  return 3;     // High
        case 4 :  return 4;     // VeryHigh
    }

    return -1;
}

function array<class <KFMonster> > BuildNextSpawnSquad(optional out int bIgnoreZEDLimit, optional out string ChosenSquadName)
{
    local array<class <KFMonster> >  ExportSquad;
    local int i,idx,RandIdx,BestIdx;
    local int NumZEDs;
    local float BestRating;
    local array<Float> SquadRatings;
    local int PriorityVal;
    local float RandMin,RandMax;
    local array<string> VeryLowSquads,LowSquads,NormalSquads,HighSquads,VeryHighSquads,SquadsToRate;
    local KFStoryGameInfo.SZEDSquadType ZSquad;
    local int j;
    local bool bSkipThisSquad;
    local array<int> SquadIndexes;

    if(GetOwningManager() != none && GetOwningManager().bPrintDebugLogs)
    {
        log("**** BUILD NEXT SQUAD *******************************************************",'Story_Debug');
    }

    if( CurrentSpawnCycle.bRandomSquadsWithoutRepeats )
    {
        if( CurrentSpawnCycle.SquadList.length == CurrentSpawnCycle.SpawnedSquads.Length )
        {
            CurrentSpawnCycle.SpawnedSquads.Length = 0;
            //log("Resetting SpawnedSquad list as we've already been through every squad",'Story_Debug');
        }

        for(i = 0 ; i < CurrentSpawnCycle.SquadList.length ; i ++)
        {
            bSkipThisSquad = false;

            // see if we should skip this squad because its already been used
            for(j = 0 ; j < CurrentSpawnCycle.SpawnedSquads.length ; j ++)
            {
                if( CurrentSpawnCycle.SpawnedSquads[j] == i )
                {
                    bSkipThisSquad = true;
                }
            }

            if( !bSkipThisSquad )
            {
                SquadsToRate[SquadsToRate.length] = CurrentSpawnCycle.SquadList[i];
                SquadIndexes[SquadsToRate.length - 1] = i;
                //log(">> Adding Squad to Randomly Cycle Through :: "@CurrentSpawnCycle.SquadList[i],'Story_Debug');
            }
            else
            {
                //log("<< Skipping already used Squad :: "@CurrentSpawnCycle.SquadList[i],'Story_Debug');
            }
        }

        BestIdx = Rand(SquadsToRate.length);
        // Add this squad to the list of squads that already have been used
        CurrentSpawnCycle.SpawnedSquads[CurrentSpawnCycle.SpawnedSquads.length] = SquadIndexes[BestIdx];
        //log("** Used Squad :: "@SquadsToRate[BestIdx],'Story_Debug');
    }
    else
    {
        /* We only want to rate one squad from each Priority pool, if we allow multiple squads from the same
        pool to be rated, it'll throw off the rating system */

        /* First of all, let's break the squadlist up by priority group */

        for(i = 0 ; i < CurrentSpawnCycle.SquadList.length ; i ++)
        {
            ZSquad = FindSquadByName(CurrentSpawnCycle.SquadList[i]);
//            log(CurrentSpawnCycle.SquadList[i]@" Last Spawned time : "@ZSquad.LastSquadSpawnTime@" :: MinSpawnInterval : "@ZSquad.MinTimeBetweenSpawns);

            /* Has enough time passed between spawns of this squad? */
            if(ZSquad.LastSquadSpawnTime > 0 && Level.TimeSeconds - ZSquad.LastSquadSpawnTime < ZSquad.MinTimeBetweenSpawns)
            {
//                log("Rejected Spawn of : "@CurrentSpawnCycle.SquadList[i]@" because it spawned : "@Level.TimeSeconds-ZSquad.LastSquadSpawnTime@" seconds ago.  Next Allowed Spawn in : "@ZSquad.MinTimeBetweenSpawns - (Level.TimeSeconds - ZSquad.LastSquadSpawnTime));
                continue;   // not long enough, skip this one.
            }

            PriorityVal = FindSquadByName(CurrentSpawnCycle.SquadList[i]).Squad_Priority;
            switch(PriorityVal)
            {
                case 0  :   NormalSquads[NormalSquads.length] = CurrentSpawnCycle.SquadList[i];               // Normal
                            break;

                case 1  :   LowSquads[LowSquads.length] = CurrentSpawnCycle.SquadList[i];                     // Low
                            break;

                case 2  :   VeryLowSquads[VeryLowSquads.length] = CurrentSpawnCycle.SquadList[i];             // VeryLow
                            break;

                case 3  :   HighSquads[HighSquads.length] = CurrentSpawnCycle.SquadList[i];                   // High
                            break;

                case 4  :   VeryHighSquads[VeryHighSquads.length] = CurrentSpawnCycle.SquadList[i];           // VeryHigh
                            break;
            }
        }

        /* Add one squad from each priority group to the Squads to rate array. */

        if(VeryLowSquads.length > 0)
        {
            SquadsToRate[SquadsToRate.length]= VeryLowSquads[Rand(VeryLowSquads.length)] ;
        }
        if(LowSquads.length > 0)
        {
            SquadsToRate[SquadsToRate.length] = LowSquads[Rand(LowSquads.length)] ;
        }
        if(NormalSquads.length > 0)
        {
            SquadsToRate[SquadsToRate.length] = NormalSquads[Rand(NormalSquads.length)] ;
        }
        if(HighSquads.length > 0)
        {
            SquadsToRate[SquadsToRate.length] = HighSquads[Rand(HighSquads.length)] ;
        }
        if(VeryHighSquads.length > 0)
        {
            SquadsToRate[SquadsToRate.length] = VeryHighSquads[Rand(VeryHighSquads.length)] ;
        }

        /* Now we're gonna calculate the rating values for each Squad */
        for(i = 0 ; i < SquadsToRate.length ; i ++)
        {
            PriorityVal = FindSquadByName(SquadsToRate[i]).Squad_Priority;

            switch(PriorityVal)
            {
                case 0  :   RandMin = 0.5;                  // Normal
                            RandMax = 1;
                            break;

                case 1  :   RandMin = 0.25;                 // Low
                            RandMax = 1;
                            break;

                case 2  :   RandMin = 0.1;                 // VeryLow
                            RandMax = 1;
                            break;

                case 3  :   RandMin = 0.75;                 // High
                            RandMax = 1;
                            break;

                case 4  :   RandMin = 0.9;                 // VeryHigh
                            RandMax = 1;
                            break;
            }

            SquadRatings[i] = RandRange(RandMin,RandMax);
        }

        /* Iterate the squad ratings and pick the highest one.  We'll be spawning that */
        for(i = 0 ; i < SquadRatings.length ; i ++)
        {
            //log("GetNextSquad ::: "@SquadsToRate[i]@" rolled a : "@SquadRatings[i],'Story_Debug');

            if(i == 0 ||
            SquadRatings[i] > BestRating)
            {
                BestRating = SquadRatings[i];
                BestIdx = i;
            }
        }

        //log("GetNextSquad ::: "@"Selected -> "@SquadsToRate[BestIdx]@" to spawn.  It's priority is : "@string(GetEnum(Enum 'KFStoryGameInfo.EZEDSpawnPriority',FindSquadByName(SquadsToRate[BestIdx]).Squad_Priority)),'Story_Debug');
    }

    RandIdx = BestIdx;
    ZSquad = FindSquadByName(SquadsToRate[RandIdx]);
    CurrentSquad = ZSquad;
    ChosenSquadName = CurrentSquad.Squad_Name;
    bIgnoreZEDLimit = int(CurrentSquad.bIgnoreLevelMaxZEDs) ;

    for(i = 0 ; i < CurrentSquad.Squad_ZEDs.Length ; i ++)
    {
        NumZEDs = CurrentSquad.Squad_ZEDS[i].NumToSpawn;
        for(idx = 0 ; idx < NumZEDs ; idx ++)
        {
            ExportSquad[ExportSquad.length] = CurrentSquad.Squad_ZEDs[i].ZEDClass ;
        }
    }

    return ExportSquad ;
}

defaultproperties
{
    bstatic = false   // so it can receive trigger events
    bHidden = true
    RemoteRole = Role_None
    CurrentCycleIdx = -1
    MaxFailedFindVolumeAttempts = 10
    MaxFailedSquadSpawnAttempts = 25
    bFinishedLastSquadSpawn=true
}
