//=============================================================================
// KFGameConductor
//=============================================================================
// Game conductor which dynamically manages the difficulty and fun of the game
//=============================================================================
// Killing Floor 2
// Copyright (C) 2016 Tripwire Interactive LLC
// John "Ramm-Jaeger" Gibson
//=============================================================================
class KFGameConductor extends Object within KFGameInfo
	config(Game);

/*********************************************************************************************
 * @name Player Status
********************************************************************************************* */

/** Represents the health status of all the human team players on the server */
var	    float           PlayersHealthStatus;
/** Represents the ammo status of all the human team players on the server */
var	    float           PlayersAmmoStatus;
/** Represents the player status of all the human team players on the server */
var	    float           AggregatePlayersStatus;
/** Team low health threshold for determining gameplay changes */
var()	float           PlayersLowHealthThreshold;

/*********************************************************************************************
 * @name Skill Tracking
********************************************************************************************* */

/** How long are zeds living on average after they have seen an enemy */
var	    float           ZedVisibleAverageLifespan;
/** Total time zeds are living after they have seen an enemy */
var	    float           TotalZedVisibleLifespan;
/** How many zeds have been killed during this match */
var	    int             TotalZedsKilled;

/** How long are zeds living on average after they have seen an enemy, for the current wave */
var	    float           CurrentWaveZedVisibleAverageLifeSpan;
/** Total time zeds are living after they have seen an enemy, for the current wave */
var	    float           CurrentWaveTotalZedVisibleLifeSpan;
/** How many zeds have been killed during this wave */
var	    int             CurrentWaveTotalZedsKilled;

/** How long are zeds living on average after they have seen an enemy, for the last 10 seconds */
var	    float           RecentZedVisibleAverageLifeSpan;
/** Total time zeds are living after they have seen an enemy, for the last 10 seconds */
var	    float           RecentTotalZedVisibleLifeSpan;
/** How many zeds have been killed during tlast 10 seconds */
var	    int             RecentTotalZedsKilled;

/** Keep track of the zed lifespan totals over the last 10 seconds */
var     float           RecentZedLifeSpanTotalTracker[10];
/** Keep track of the zed kill totals over the last 10 seconds */
var     float           RecentZedKillTotalTracker[10];

/** Represents the player skill level off all the human team players on the server */
var	    float           AggregatePlayerSkill;

/** Represents the average shooting accuracy of all the human team players on the server */
var	    float           PlayersAverageAccuracy;
/** Represents the average headshot accuracy of all the human team players on the server */
var	    float           PlayersAverageHeadshotAccuracy;
/** What we expect the average player shot accuracy to be */
var	    float           ParShotAccuracy;
/** What we expect the average player shot accuracy to be */
var	    float           ParHeadshotAccuracy;

/** What percentage above BaseLinePlayerShootingSkill to consider a players shooting accuracy "highly skilled" */
var()	float           HighlySkilledAccuracyMod;
/** What percentage above BaseLinePlayerShootingSkill to use as the max value for calculating the skill modifer */
var()	float           HighlySkilledAccuracyModMax;
/** What percentage below BaseLinePlayerShootingSkill to consider a players shooting accuracy "less skilled" */
var()	float           LessSkilledAccuracyMod;
/** What percentage below BaseLinePlayerShootingSkill to use as the min value for calculating the skill modifer */
var()	float           LessSkilledAccuracyModMin;

/** What is the baseline player shooting skill value */
var 	float           BaseLinePlayerShootingSkill;
/** What percentage weight should general shooting accuracy have for the BaseLinePlayerShootingSkill */
var()	float           ShootingAccuracySkillWeight;
/** What percentage weight should headshot accuracy have for the BaseLinePlayerShootingSkill */
var()	float           HeadShootingAccuracySkillWeight;

/** What we expect the average zed lifespan to be, by difficulty */
var	    float           ParZedLifeSpan[4];
/** What we expect the average zed lifespan to be, by difficulty on solo*/
var	    float           ParZedLifeSpanSolo[4];

/** What percentage of ParZedLifeSpan to consider how quickly zeds are being killed as "highly skilled" */
var()	float           ZedLifeSpanHighlySkilledThreshold;
/** What percentage of ParZedLifeSpan to use as the min value for calculating the skill modife */
var()	float           ZedLifeSpanHighlySkilledThresholdMin;
/** What percentage of ParZedLifeSpan to consider how quickly zeds are being killed as "less skilled" */
var()	float           ZedLifeSpanLessSkilledThreshold;
/** What percentage of ParZedLifeSpan to use as the max value for calculating the skill modifer */
var()	float           ZedLifeSpanLessSkilledThresholdMax;

/** The overall modifier to use based on the players' perk rank and playing skill */
var	    float           OverallRankAndSkillModifier;
/** What percentage of OverallRankAndSkillModifier does PerkRank count toward */
var()	float           PerkRankPercentOfOverallSkill;
/** What percentage of OverallRankAndSkillModifier does shooting accuracy count toward */
var()	float           AccuracyPercentOfOverallSkill;
/** What percentage of OverallRankAndSkillModifier does zed lifespan count toward */
var()	float           ZedLifeSpanPercentOfOverallSkill;

/*********************************************************************************************
 * @name Game Status
**********************************************************************************************/

enum EGameConductorStatus
{
	GCS_Normal,
	GCS_ForceLull
};

/** Current overall status of the game conductor */
var EGameConductorStatus GameConductorStatus;

/** How long to force a low intensity lull for if a player dies */
var()	float           PlayerDeathForceLullLength;
/** When we lasted forced a low intensity lull for a player dying */
var()	float           PlayerDeathForceLullTime;

/** How long to force a low intensity lull for if a solo player is surrounded */
var()   float           SoloPlayerSurroundedForceLullLength;
/** When we lasted forced a low intensity lull for a surrounded solo player */
var()   float           SoloPlayerSurroundedForceLullTime;

/*********************************************************************************************
 * @name Player Experience Level
**********************************************************************************************/

/** The average of the perk rank for all the players on the server */
var	float           AveragePlayerPerkRank;

/** The target perk range for each difficulty level */
var() vector2d      TargetPerkRankRange[4];

/** What is the median target perk rank for the current game */
var float CurrentTargetPerkRank;

/*********************************************************************************************
 * @name Gameplay modification
********************************************************************************************* */

/** Struct containing settings for the lull state */
struct sLullInfo
{
    /** Maximum allowed lull duration */
    var float MaxDuration;
    /** Cooldown until next lull period is allowed */
    var float Cooldown;
};

/** How much the spawn rate can be modified by difficulty based on perk rank and how well the team is doing. At 0 it will be as slow is you want it to go, at 1.0 as fast as you want it to go, and at 0.5 should be no change */
var() InterpCurveFloat      SpawnRateModificationRangeCurve[4];

/** Current spawn rate modification */
var() float         CurrentSpawnRateModification;

/**
  * What percentage of the difference between the zed movement speed on the current
  * difficulty, and the zed movement speed on the next highest difficulty, do we want
  * to add/subtract to the movement speed based on how well/poorly a team is doing.
  * X = the % difference to use for slowing zeds down if a team is doing poorly,
  * Y =  the % difference to use for speeding up zeds if the team is doing well.
  * Valid values for X are between 0 (no change) and 0.9. Valid values for Y are
  * between 0 (no change) and 1.0. For example, using 0.5 for X and Y would give
  * you an increase or decrease in movement speed halfway in between the speed of
  * the zeds on the current difficulty, and the speed of the zeds on the next higher
  * difficulty.
  */
var() vector2d      AIMovementSpeedModificationRange[4];

/** Current AI Movement speed modifier */
var     float           CurrentAIMovementSpeedMod;
/** Current modification the game conductor is making to versus player zed health */
var	    float           CurrentVersusZedHealthMod;
/** Current modification the game conductor is making to versus player zed damage */
var	    float           CurrentVersusZedDamageMod;

/** Whether or not this game difficulty allows low intensity zed mode when players get low on health, forced lull, etc */
var()   int             AllowLowIntensityZedModeByDifficulty[4];

/** Per-difficulty settings for the lull state */
var() array<sLullInfo>  LullSettings;
/** Last time the lull cooldown was triggered */
var transient float     LullCooldownStartTime;

/*********************************************************************************************
 * @name Logging and debugging
********************************************************************************************* */

/** Whether or not to log Game Conductor activity */
var() bool          bLogGameConductor;

/** When true bypass the game conductor making any adjustments */
var() bool          bBypassGameConductor;

/*********************************************************************************************
 * @name Player Status
********************************************************************************************* */

/** Initialize the game conductor */
function Initialize()
{
    BaseLinePlayerShootingSkill = ParShotAccuracy * ShootingAccuracySkillWeight + ParHeadshotAccuracy * HeadShootingAccuracySkillWeight;
}

/*********************************************************************************************
 * @name Skill Tracking
********************************************************************************************* */

/** Update stats when a zed is killed */
function HandleZedKill( float ZedVisibleTimeAlive )
{
    TotalZedsKilled++;
    TotalZedVisibleLifespan += ZedVisibleTimeAlive;
    ZedVisibleAverageLifespan = TotalZedVisibleLifespan/float(TotalZedsKilled);

    CurrentWaveTotalZedsKilled++;
    CurrentWaveTotalZedVisibleLifeSpan += ZedVisibleTimeAlive;
    CurrentWaveZedVisibleAverageLifeSpan = CurrentWaveTotalZedVisibleLifeSpan/float(CurrentWaveTotalZedsKilled);

    RecentTotalZedsKilled++;
    RecentTotalZedVisibleLifeSpan += ZedVisibleTimeAlive;

    //`log("TotalZedsKilled = "$TotalZedsKilled$" TotalZedVisibleLifespan = "$TotalZedVisibleLifespan$" ZedVisibleAverageLifespan = "$ZedVisibleAverageLifespan$" seconds");
    //`log("CurrentWaveTotalZedsKilled = "$CurrentWaveTotalZedsKilled$" CurrentWaveZedVisibleAverageLifeSpan = "$CurrentWaveZedVisibleAverageLifeSpan$" CurrentWaveZedVisibleAverageLifeSpan = "$CurrentWaveZedVisibleAverageLifeSpan$" seconds");
}

/** Let the game conductor know a human team pawn just died */
function NotifyHumanTeamPlayerDeath()
{
    if( !MyKFGRI.IsBossWave() )
    {
        `log("Human team player died, forcing a lull for "$PlayerDeathForceLullLength$" seconds!", bLogGameConductor);

        GameConductorStatus = GCS_ForceLull;
        PlayerDeathForceLullTime = WorldInfo.TimeSeconds;
    }
}

/** Let the game conductor know that the solo player has been surrounded */
function NotifySoloPlayerSurrounded()
{
    if( !MyKFGRI.IsBossWave() && GameConductorStatus != GCS_ForceLull )
    {
        `log("Human solo player surrounded, forcing a lull for "$SoloPlayerSurroundedForceLullLength$" seconds!", bLogGameConductor);

        GameConductorStatus = GCS_ForceLull;
        SoloPlayerSurroundedForceLullTime = WorldInfo.TimeSeconds;
    }
}

/** Reset stats that are collected for a single wave */
function ResetWaveStats()
{
    CurrentWaveTotalZedsKilled=0;
    CurrentWaveTotalZedVisibleLifeSpan=0;
    CurrentWaveZedVisibleAverageLifeSpan=0;
}

/** Update the average perk rank of the players on the server */
function UpdateAveragePerkRank()
{
	local KFPlayerController KFPC;
	local KFPerk MyPerk;
	local int NumHumanTeamPlayers;
	local float TotalPerkRank;

	foreach WorldInfo.AllControllers( class'KFPlayerController', KFPC )
	{
		if( KFPC.GetTeamNum() == 0 )
		{
            MyPerk = KFPC.CurrentPerk;

    		if( MyPerk != none )
    		{
                //`log(KFPC$" MyPerk = "$MyPerk$" MyPerk.GetLevel() = "$MyPerk.GetLevel());
                NumHumanTeamPlayers++;
                TotalPerkRank += Float(MyPerk.GetLevel());
    		}
		}
	}

	if( NumHumanTeamPlayers > 0 )
	{
        AveragePlayerPerkRank = TotalPerkRank/NumHumanTeamPlayers;
        //`log(self@GetFuncName()$" AveragePlayerPerkRank = "$AveragePlayerPerkRank$" TotalPerkRank = "$TotalPerkRank$" NumHumanTeamPlayers = "$NumHumanTeamPlayers);
	}
	else
	{
        AveragePlayerPerkRank = 0;
        //`log(self@GetFuncName()$" AveragePlayerPerkRank = "$AveragePlayerPerkRank$" TotalPerkRank = "$TotalPerkRank$" NumHumanTeamPlayers = "$NumHumanTeamPlayers);
	}

}

/** Handle someone switching teams */
function HandlePlayerChangedTeam()
{
    // Perk levels are now valid handle that
    UpdateAveragePerkRank();
}

/** Called once per second to evaluate changes to gameplay */
function TimerUpdate()
{
    UpdatePlayerAccuracyStats();
    UpdateZedLifespanStats();
    UpdatePlayersStatus();
    UpdatePlayersAggregateSkill();

    UpdateOverallStatus();

    EvaluateSpawnRateModification();
    EvaluateAIMovementSpeedModification();
}

/** Update the tracking of the status of the players on the server */
function UpdatePlayersStatus()
{
	local Controller C;
	local KFPawn_Human KFP;
	local float TotalHealth, TotalAmmo, TotalMaxAmmo;
	local KFInventoryManager KFIM;
	local int i;
	local KFPlayerReplicationInfo KFPRI;
	local float NumHumanPlayers;

    UpdateAveragePerkRank();

	foreach WorldInfo.AllControllers( class'Controller', C )
	{
		if( C.GetTeamNum() == 0 )
		{
            // Add up the health of living players
            if( C.Pawn != none )
            {
                KFP =  KFPawn_Human(C.Pawn);

                if( KFP != none && KFPawn_Customization(KFP) == none )
                {
                    TotalHealth += Float(KFP.Health)/Float(KFP.HealthMax);
                    NumHumanPlayers += 1.0;
                    //`log("C = "$C$" KFP.Health = "$KFP.Health$" KFP.HealthMax = "$KFP.HealthMax$" C.Pawn = "$C.Pawn);

                	KFIM = KFInventoryManager(KFP.InvManager);
                	if (KFIM !=none)
                	{
                        TotalAmmo += KFIM.GetPrimaryAmmoPercentage();
                        TotalMaxAmmo += 1.0;
                    }
                }
            }
            // Add up the health (or lack thereof) of players that don't have a pawn, so have
            // already died. Doesn't count newly joined players waiting to spawn.
            else if( C.PlayerReplicationInfo != none && !C.PlayerReplicationInfo.bOnlySpectator &&
                !C.PlayerReplicationInfo.bWaitingPlayer )
            {
                KFPRI = KFPlayerReplicationInfo(C.PlayerReplicationInfo);

                if( KFPRI != none )
                {
                    //`log("C = "$C$" KFPRI.PlayerHealthPercent = "$ByteToFloat(KFPRI.PlayerHealthPercent)$" KFP.HealthMax = 100");
                    TotalHealth += ByteToFloat(KFPRI.PlayerHealthPercent);
                    NumHumanPlayers += 1.0;
                }
            }
		}
	}

	if( NumHumanPlayers > 0 )
	{
        PlayersHealthStatus = TotalHealth/NumHumanPlayers;
    }
    else
    {
        PlayersHealthStatus = 0;
    }

	if( TotalMaxAmmo > 0 )
	{
        PlayersAmmoStatus = TotalAmmo/TotalMaxAmmo;
    }
    else
    {
        PlayersAmmoStatus = 0;
    }

    // Aggregates all player status variables into one value
    AggregatePlayersStatus = (PlayersHealthStatus * 0.5) + (PlayersAmmoStatus * 0.5);

	//`log("PlayersHealthStatus = "$PlayersHealthStatus$" PlayersAmmoStatus = "$PlayersAmmoStatus$" AggregatePlayerStatus = "$AggregatePlayersStatus);

	for( i = 0; i < (ArrayCount(MyKFGRI.PlayersHealthStatusTracker) - 1); i++ )
	{
	   MyKFGRI.PlayersHealthStatusTracker[i] = MyKFGRI.PlayersHealthStatusTracker[i+1];
	   MyKFGRI.PlayersAmmoStatusTracker[i] = MyKFGRI.PlayersAmmoStatusTracker[i+1];
	   MyKFGRI.AggregatePlayersStatusTracker[i] = MyKFGRI.AggregatePlayersStatusTracker[i+1];
	}

    MyKFGRI.PlayersHealthStatusTracker[ArrayCount(MyKFGRI.PlayersHealthStatusTracker) -1] = PlayersHealthStatus;
    MyKFGRI.PlayersAmmoStatusTracker[ArrayCount(MyKFGRI.PlayersAmmoStatusTracker) -1] = PlayersAmmoStatus;
    MyKFGRI.AggregatePlayersStatusTracker[ArrayCount(MyKFGRI.AggregatePlayersStatusTracker) -1] = AggregatePlayersStatus;
}

/** Update the tracking of the status of the players on the server */
function UpdatePlayersAggregateSkill()
{
	local int i;

    // Heuristic for how players are performing.
    AggregatePlayerSkill = PlayersAverageAccuracy * 0.25 + PlayersAverageHeadshotAccuracy * 0.75;

	for( i = 0; i < (ArrayCount(MyKFGRI.AggregatePlayerSkillTracker) - 1); i++ )
	{
	   MyKFGRI.AggregatePlayerSkillTracker[i] = MyKFGRI.AggregatePlayerSkillTracker[i+1];
	}

    MyKFGRI.AggregatePlayerSkillTracker[ArrayCount(MyKFGRI.AggregatePlayerSkillTracker) -1] = AggregatePlayerSkill;
}

/** Update the accuracy stats of the players on the server */
function UpdatePlayerAccuracyStats()
{
	local KFPlayerController KFPC;
	local int NumHumanTeamPlayers;
	local float TotalAccuracy, TotalHeadShotAccuracy;
	local int i;

	foreach WorldInfo.AllControllers( class'KFPlayerController', KFPC )
	{
		if( KFPC.GetTeamNum() == 0 && KFPC.GetPerk() != none )
		{
            NumHumanTeamPlayers++;
            if( KFPC.ShotsFired > 0 )
            {
                TotalAccuracy += (Float(KFPC.ShotsHit)/Float(KFPC.ShotsFired) * 100.0) + KFPC.GetPerk().HitAccuracyHandicap;
                TotalHeadShotAccuracy += (Float(KFPC.ShotsHitHeadshot)/Float(KFPC.ShotsFired) * 100.0) + KFPC.GetPerk().HeadshotAccuracyHandicap;
            }
		}
	}

	for( i = 0; i < (ArrayCount(MyKFGRI.PlayerAccuracyTracker) - 1); i++ )
	{
	   MyKFGRI.PlayerAccuracyTracker[i] = MyKFGRI.PlayerAccuracyTracker[i+1];
	   MyKFGRI.PlayerHeadshotAccuracyTracker[i] = MyKFGRI.PlayerHeadshotAccuracyTracker[i+1];
	}

    if( NumHumanTeamPlayers > 0 )
    {
        PlayersAverageAccuracy = TotalAccuracy/float(NumHumanTeamPlayers);
        PlayersAverageHeadshotAccuracy = TotalHeadShotAccuracy/float(NumHumanTeamPlayers);

        MyKFGRI.PlayerAccuracyTracker[ArrayCount(MyKFGRI.PlayerAccuracyTracker) -1] = PlayersAverageAccuracy;
        MyKFGRI.PlayerHeadshotAccuracyTracker[ArrayCount(MyKFGRI.PlayerHeadshotAccuracyTracker) -1] = PlayersAverageHeadshotAccuracy;
    }
}

/** Update the stats on how long zeds are surviving on the server */
function UpdateZedLifespanStats()
{
	local int i;
	local float CurrentTotalLifeSpan;
	local int CurrentTotalKills;
	local float TotalRecentAverageLifespan;

	for( i = 0; i < (ArrayCount(MyKFGRI.TotalZedLifeSpanAverageTracker) - 1); i++ )
	{
        CurrentTotalLifeSpan += RecentZedLifeSpanTotalTracker[i];
        CurrentTotalKills += RecentZedKillTotalTracker[i];
        if( i > 6 )
        {
            TotalRecentAverageLifespan += MyKFGRI.RecentZedLifeSpanAverageTracker[i+1];
        }

        MyKFGRI.TotalZedLifeSpanAverageTracker[i] = MyKFGRI.TotalZedLifeSpanAverageTracker[i+1];
        MyKFGRI.CurrentWaveZedLifeSpanAverageTracker[i] = MyKFGRI.CurrentWaveZedLifeSpanAverageTracker[i+1];
        MyKFGRI.RecentZedLifeSpanAverageTracker[i] = MyKFGRI.RecentZedLifeSpanAverageTracker[i+1];
        RecentZedKillTotalTracker[i] = RecentZedKillTotalTracker[i+1];
        RecentZedLifeSpanTotalTracker[i] = RecentZedLifeSpanTotalTracker[i+1];
	}

    MyKFGRI.TotalZedLifeSpanAverageTracker[ArrayCount(MyKFGRI.TotalZedLifeSpanAverageTracker) -1] = ZedVisibleAverageLifespan;
    MyKFGRI.CurrentWaveZedLifeSpanAverageTracker[ArrayCount(MyKFGRI.CurrentWaveZedLifeSpanAverageTracker) -1] = CurrentWaveZedVisibleAverageLifeSpan;

    // Keep track of recent kills
    CurrentTotalLifeSpan += RecentTotalZedVisibleLifeSpan;
    CurrentTotalKills += RecentTotalZedsKilled;
    RecentZedLifeSpanTotalTracker[ArrayCount(RecentZedLifeSpanTotalTracker) -1] = RecentTotalZedVisibleLifeSpan;
    RecentZedKillTotalTracker[ArrayCount(RecentZedKillTotalTracker) -1] = RecentTotalZedsKilled;
    if( CurrentTotalKills > 0 )
    {
        RecentZedVisibleAverageLifeSpan = CurrentTotalLifeSpan/float(CurrentTotalKills);
        TotalRecentAverageLifespan += RecentZedVisibleAverageLifeSpan;

        // Smooth the value with an average over the last 3 values
        MyKFGRI.RecentZedLifeSpanAverageTracker[ArrayCount(MyKFGRI.RecentZedLifeSpanAverageTracker) -1] = TotalRecentAverageLifespan/3.0;
    }
    else
    {
        // No recent kills, just populate with the last checks values
        RecentZedVisibleAverageLifeSpan = MyKFGRI.RecentZedLifeSpanAverageTracker[ArrayCount(MyKFGRI.RecentZedLifeSpanAverageTracker) -2];
        MyKFGRI.RecentZedLifeSpanAverageTracker[ArrayCount(MyKFGRI.RecentZedLifeSpanAverageTracker) -1] = RecentZedVisibleAverageLifeSpan;
    }

    // Clear the recent kill/lifespawn info
    RecentTotalZedVisibleLifeSpan = 0;
    RecentTotalZedsKilled = 0;
}

/** Returns what we expect the average zed lifespan to be, by difficulty */
function float GetParZedLifeSpan()
{
    if( !bOnePlayerAtStart )
    {
        return ParZedLifeSpan[GameDifficulty];

    }
    else
    {
        return ParZedLifeSpanSolo[GameDifficulty];
    }
}

/** Calculate the overall status of the player's rank and performance */
function UpdateOverallStatus()
{
    local float PerkRankModifier;
    local float SkillModifier;
    local float LifeSpanModifier;
    local float HighlySkilledAccuracy, LessSkilledAccuracy;
    local float HighlySkilledZedLifespan, LessSkilledZedLifespan;
    local bool bPlayerHealthLow;
    local int i;

    // Take us out of a forced lull if the time is up
    if( GameConductorStatus == GCS_ForceLull
        && `TimeSince(PlayerDeathForceLullTime) > PlayerDeathForceLullLength
        && `TimeSince(SoloPlayerSurroundedForceLullTime) > SoloPlayerSurroundedForceLullLength )
    {
        GameConductorStatus = GCS_Normal;
        `log("Forced lull completed", bLogGameConductor);
    }

    MyKFGRI.CurrentGameConductorStatus = GameConductorStatus;
    MyKFGRI.CurrentParZedLifeSpan = GetParZedLifeSpan();

	for( i = 0; i < (ArrayCount(MyKFGRI.OverallRankAndSkillModifierTracker) - 1); i++ )
	{
        MyKFGRI.OverallRankAndSkillModifierTracker[i] = MyKFGRI.OverallRankAndSkillModifierTracker[i+1];
	}

    // Bypassing making game conductor adjustments
    if( bBypassGameConductor || MyKFGRI.IsBossWave() )
    {
        OverallRankAndSkillModifier = 0.5;
        `log("Bypassing GameConductor adjustment OverallRankAndSkillModifier = "$OverallRankAndSkillModifier, bLogGameConductor);
        MyKFGRI.OverallRankAndSkillModifierTracker[ArrayCount(MyKFGRI.OverallRankAndSkillModifierTracker) -1] = OverallRankAndSkillModifier;
        return;
    }

    // Forced lull, or most of the team dead, or single player nearly dead, so slow things down
    bPlayerHealthLow = PlayersHealthStatus < PlayersLowHealthThreshold;
    if( GameConductorStatus == GCS_ForceLull
        || (bPlayerHealthLow && (LullCooldownStartTime == 0.f || `TimeSince(LullCooldownStartTime) > LullSettings[GameDifficulty].Cooldown)) )
    {
        OverallRankAndSkillModifier = 0.0;
        `log("Players low on health PlayersHealthStatus: "$PlayersHealthStatus$" chilling things out, OverallRankAndSkillModifier= "$OverallRankAndSkillModifier, bLogGameConductor);
        MyKFGRI.OverallRankAndSkillModifierTracker[ArrayCount(MyKFGRI.OverallRankAndSkillModifierTracker) -1] = OverallRankAndSkillModifier;

        // Start the lull timer. Don't allow lulls to last too long
        if( bPlayerHealthLow && !MyKFGRI.IsTimerActive(nameOf(Timer_EndLull), self) )
        {
            MyKFGRI.SetTimer( LullSettings[GameDifficulty].MaxDuration, false, nameOf(Timer_EndLull), self );
        }

        return;
    }

    // No longer in a lull, reset duration timer
    MyKFGRI.ClearTimer( nameOf(Timer_EndLull), self );

    if( WithinRange(TargetPerkRankRange[GameDifficulty],AveragePlayerPerkRank) )
    {
        PerkRankModifier = GetRangePctByValue( TargetPerkRankRange[GameDifficulty], AveragePlayerPerkRank );
    }
    else if( AveragePlayerPerkRank < TargetPerkRankRange[GameDifficulty].X )
    {
        PerkRankModifier = 0;
    }
    else
    {
        PerkRankModifier = 1;
    }

    // Evaluate player skill if its greater than 15 seconds into the match,
    // so you have some data to go by
    if( MyKFGRI != none && MyKFGRI.ElapsedTime > 15 && AggregatePlayerSkill != 0 )
    {
        HighlySkilledAccuracy = BaseLinePlayerShootingSkill * HighlySkilledAccuracyMod;
        LessSkilledAccuracy = BaseLinePlayerShootingSkill * LessSkilledAccuracyMod;

        if( AggregatePlayerSkill > HighlySkilledAccuracy )
        {
            // Highly skilled players
            SkillModifier = Lerp(0.51,1.0, FMin(1.0,(AggregatePlayerSkill - HighlySkilledAccuracy)/((BaseLinePlayerShootingSkill * HighlySkilledAccuracyModMax) - HighlySkilledAccuracy)));
        }
        else if( AggregatePlayerSkill < LessSkilledAccuracy )
        {
            // Less skilled players
            SkillModifier = Lerp(0.49,0.0, FMax(0,(LessSkilledAccuracy - AggregatePlayerSkill)/(LessSkilledAccuracy - (BaseLinePlayerShootingSkill * LessSkilledAccuracyModMin))));
        }
        else
        {
            // Standard skilled players
            SkillModifier = 0.5;
        }
    }
    else
    {
        // Standard skilled players
        SkillModifier = 0.5;
    }

    if( RecentZedVisibleAverageLifeSpan > 0 )
    {
        HighlySkilledZedLifespan = GetParZedLifeSpan() * ZedLifeSpanHighlySkilledThreshold;
        LessSkilledZedLifespan = GetParZedLifeSpan() * ZedLifeSpanLessSkilledThreshold;

        if( RecentZedVisibleAverageLifeSpan < HighlySkilledZedLifespan )
        {
            // Highly skilled players
            LifeSpanModifier = Lerp(0.51,1.0, FMin(1.0,(HighlySkilledZedLifespan - RecentZedVisibleAverageLifeSpan)/( HighlySkilledZedLifespan - (GetParZedLifeSpan() * ZedLifeSpanHighlySkilledThresholdMin))));

        }
        else if( RecentZedVisibleAverageLifeSpan > LessSkilledZedLifespan )
        {
            // Less skilled players
            LifeSpanModifier = Lerp(0.49,0.0, FMin(1.0,(RecentZedVisibleAverageLifeSpan - LessSkilledZedLifespan)/((GetParZedLifeSpan() * ZedLifeSpanLessSkilledThresholdMax) - LessSkilledZedLifespan)));
        }
        else
        {
            // Standard skilled players
            LifeSpanModifier = 0.5;
        }
    }
    else
    {
        // Standard skilled players
        LifeSpanModifier = 0.5;
    }

    OverallRankAndSkillModifier = PerkRankModifier * PerkRankPercentOfOverallSkill + SkillModifier * AccuracyPercentOfOverallSkill + LifeSpanModifier * ZedLifeSpanPercentOfOverallSkill;
    MyKFGRI.OverallRankAndSkillModifierTracker[ArrayCount(MyKFGRI.OverallRankAndSkillModifierTracker) -1] = OverallRankAndSkillModifier;

    `log("PerkRankModifier = "$PerkRankModifier$" SkillModifier = "$SkillModifier$" LifeSpanModifier = "$LifeSpanModifier$" OverallRankAndSkillModifier= "$OverallRankAndSkillModifier$" GetParZedLifeSpan() = "$GetParZedLifeSpan(), bLogGameConductor);
}

/** Called when a zed does an attack to give an opportunity to scale their attack cooldowns */
function UpdateOverallAttackCoolDowns(KFAIController KFAIC)
{
    local bool bAllowLowZedIntensity;

    if( GameDifficulty < ArrayCount(AllowLowIntensityZedModeByDifficulty) )
    {
        bAllowLowZedIntensity = (AllowLowIntensityZedModeByDifficulty[GameDifficulty] == 1);
    }
    else
    {
        bAllowLowZedIntensity = (AllowLowIntensityZedModeByDifficulty[ArrayCount(AllowLowIntensityZedModeByDifficulty) - 1] == 1);
    }

    if( !bBypassGameConductor && bAllowLowZedIntensity && !MyKFGRI.IsBossWave() )
    {
        if( GameConductorStatus == GCS_ForceLull || OverallRankAndSkillModifier == 0 )
        {
            KFAIC.SetOverallCooldownTimer(KFAIC.LowIntensityAttackCooldown);
        }
        else
        {
            KFAIC.SetOverallCooldownTimer(0.0);
        }
    }
}

/** Potentially modify the spawn rate based on various factors */
function EvaluateSpawnRateModification()
{
    local int i;

    CurrentSpawnRateModification = EvalInterpCurveFloat(SpawnRateModificationRangeCurve[GameDifficulty], OverallRankAndSkillModifier);

    if( MyKFGRI.bGameConductorGraphingEnabled )
    {
    	for( i = 0; i < (ArrayCount(MyKFGRI.ZedSpawnRateModifierTracker) - 1); i++ )
    	{
            MyKFGRI.ZedSpawnRateModifierTracker[i] = MyKFGRI.ZedSpawnRateModifierTracker[i+1];
            MyKFGRI.ZedSpawnRateTracker[i] = MyKFGRI.ZedSpawnRateTracker[i+1];
    	}

        MyKFGRI.ZedSpawnRateModifierTracker[ArrayCount(MyKFGRI.ZedSpawnRateModifierTracker) -1] = CurrentSpawnRateModification;
        MyKFGRI.ZedSpawnRateTracker[ArrayCount(MyKFGRI.ZedSpawnRateTracker) -1] = MyKFGRI.CurrentNextSpawnTime;
    }

    `log("CurrentSpawnRateModification = "$CurrentSpawnRateModification$" MyKFGRI.ElapsedTime "$MyKFGRI.ElapsedTime, bLogGameConductor);
}

/** Potentially modify the AI movement speed based on various factors */
function EvaluateAIMovementSpeedModification()
{
    local float DifficultySpeedAdjustMod, BaseGroundSpeedMod;
	local KFPawn_Monster KFPM;
    local float UsedMovementSpeedPercentIncrease, UsedMovementSpeedPercentDecrease;
    local float UsedMovementSpeedPercent;
    local int i;

    if( GameDifficulty < ArrayCount(AIMovementSpeedModificationRange) )
    {
        UsedMovementSpeedPercentDecrease = AIMovementSpeedModificationRange[GameDifficulty].X;
        UsedMovementSpeedPercentIncrease = AIMovementSpeedModificationRange[GameDifficulty].Y;
    }
    else
    {
        UsedMovementSpeedPercentDecrease = AIMovementSpeedModificationRange[ArrayCount(AIMovementSpeedModificationRange) - 1].X;
        UsedMovementSpeedPercentIncrease = AIMovementSpeedModificationRange[ArrayCount(AIMovementSpeedModificationRange) - 1].Y;
    }

    // Clamp the values to reasonable settings
    UsedMovementSpeedPercentDecrease = FClamp( UsedMovementSpeedPercentDecrease, 0.0, 0.9 );
    UsedMovementSpeedPercentIncrease = FClamp( UsedMovementSpeedPercentIncrease, 0.0, 1.0 );

    BaseGroundSpeedMod = DifficultyInfo.GetAIGroundSpeedMod();

    if( OverallRankAndSkillModifier < 0.5 )
    {
        UsedMovementSpeedPercent = UsedMovementSpeedPercentDecrease;
    }
    else if( OverallRankAndSkillModifier > 0.5 )
    {
        UsedMovementSpeedPercent = UsedMovementSpeedPercentIncrease;
    }
    else
    {
        UsedMovementSpeedPercent = 0;
    }

    if( UsedMovementSpeedPercent > 0 )
    {
        if(  GameDifficulty <= 0 ) // Normal
        {
            DifficultySpeedAdjustMod = (DifficultyInfo.Hard.MovementSpeedMod - DifficultyInfo.Normal.MovementSpeedMod) * UsedMovementSpeedPercent;
        }
        else if(  GameDifficulty <= 1 ) // Hard
        {
            DifficultySpeedAdjustMod = (DifficultyInfo.Suicidal.MovementSpeedMod - DifficultyInfo.Hard.MovementSpeedMod) * UsedMovementSpeedPercent;
        }
        else  // Suicidal and HOE
        {
            DifficultySpeedAdjustMod = (DifficultyInfo.HellOnEarth.MovementSpeedMod - DifficultyInfo.Suicidal.MovementSpeedMod) * UsedMovementSpeedPercent;
        }

        if( OverallRankAndSkillModifier < 0.5 )
        {
            CurrentAIMovementSpeedMod = BaseGroundSpeedMod - (DifficultySpeedAdjustMod * (0.5 - OverallRankAndSkillModifier) * 2.0);
        }
        else if( OverallRankAndSkillModifier > 0.5 )
        {
            CurrentAIMovementSpeedMod = BaseGroundSpeedMod + (DifficultySpeedAdjustMod * (OverallRankAndSkillModifier - 0.5) * 2.0);
        }
        else
        {
            CurrentAIMovementSpeedMod = BaseGroundSpeedMod;
        }
    }
    else
    {
        CurrentAIMovementSpeedMod = BaseGroundSpeedMod;
    }

    if( MyKFGRI.bGameConductorGraphingEnabled )
    {
    	for( i = 0; i < (ArrayCount(MyKFGRI.ZedMovementSpeedModifierTracker) - 1); i++ )
    	{
            MyKFGRI.ZedMovementSpeedModifierTracker[i] = MyKFGRI.ZedMovementSpeedModifierTracker[i+1];
    	}

        MyKFGRI.ZedMovementSpeedModifierTracker[ArrayCount(MyKFGRI.ZedMovementSpeedModifierTracker) -1] = CurrentAIMovementSpeedMod;
    }

    // Adjust the speed of the currently living pawns
	foreach WorldInfo.AllPawns( class'KFPawn_Monster', KFPM )
	{
        if( KFPM.Health > 0 )
        {
            KFPM.AdjustMovementSpeed(CurrentAIMovementSpeedMod);
            `log("Adjust KFPM.default.GroundSpeed = "$KFPM.default.GroundSpeed$" CurrentAIMovementSpeedMod = "$CurrentAIMovementSpeedMod$" percent of default = "$(KFPM.default.GroundSpeed * CurrentAIMovementSpeedMod)/KFPM.default.GroundSpeed, bLogGameConductor);
        }
	}

    `log("CurrentAIMovementSpeedMod = "$CurrentAIMovementSpeedMod$" DifficultyInfo.GetAIGroundSpeedMod() = "$DifficultyInfo.GetAIGroundSpeedMod(), bLogGameConductor);
}

function bool WithinRange( vector2d Range, float TestValue )
{
    if( TestValue >= Range.X && TestValue <= Range.Y )
    {
        return true;
    }

    return false;
}

/** Ends the lull period and puts it on cooldown */
function Timer_EndLull()
{
    LullCooldownStartTime = WorldInfo.TimeSeconds;
}

defaultproperties
{
    TargetPerkRankRange(`DIFFICULTY_NORMAL)     =(X=0,Y=7)
    TargetPerkRankRange(`DIFFICULTY_HARD)       =(X=0,Y=12)
    TargetPerkRankRange(`DIFFICULTY_SUICIDAL)   =(X=12,Y=25)
    TargetPerkRankRange(`DIFFICULTY_HELLONEARTH)=(X=24.999,Y=25)

    SpawnRateModificationRangeCurve(`DIFFICULTY_NORMAL)     =(Points=((InVal=0.f,OutVal=1.25f),(InVal=0.5f, OutVal=1.0),(InVal=1.f, OutVal=0.75f)))
    SpawnRateModificationRangeCurve(`DIFFICULTY_HARD)       =(Points=((InVal=0.f,OutVal=1.25f),(InVal=0.5f, OutVal=1.0),(InVal=1.f, OutVal=0.75f)))
    SpawnRateModificationRangeCurve(`DIFFICULTY_SUICIDAL)   =(Points=((InVal=0.f,OutVal=1.25f),(InVal=0.5f, OutVal=1.0),(InVal=1.f, OutVal=0.5f)))
    SpawnRateModificationRangeCurve(`DIFFICULTY_HELLONEARTH)=(Points=((InVal=0.f,OutVal=1.0f),(InVal=0.5f, OutVal=1.0),(InVal=1.f, OutVal=0.75f)))

    AIMovementSpeedModificationRange(`DIFFICULTY_NORMAL)        =(X=0.5,Y=0.5)
    AIMovementSpeedModificationRange(`DIFFICULTY_HARD)          =(X=0.5,Y=0.5)
    AIMovementSpeedModificationRange(`DIFFICULTY_SUICIDAL)      =(X=0.5,Y=0.65)
    AIMovementSpeedModificationRange(`DIFFICULTY_HELLONEARTH)   =(X=0.0,Y=0.0)

    AllowLowIntensityZedModeByDifficulty(`DIFFICULTY_NORMAL)        =1
    AllowLowIntensityZedModeByDifficulty(`DIFFICULTY_HARD)          =1
    AllowLowIntensityZedModeByDifficulty(`DIFFICULTY_SUICIDAL)      =1
    AllowLowIntensityZedModeByDifficulty(`DIFFICULTY_HELLONEARTH)   =0

    CurrentSpawnRateModification=1.0
    CurrentAIMovementSpeedMod=1.0
    CurrentVersusZedHealthMod=1.0
    CurrentVersusZedDamageMod=1.0

    ParShotAccuracy=48.0
    ParHeadshotAccuracy=10.0
    ParZedLifeSpan(`DIFFICULTY_NORMAL)      =35.0
    ParZedLifeSpan(`DIFFICULTY_HARD)        =32.0
    ParZedLifeSpan(`DIFFICULTY_SUICIDAL)    =28.0
    ParZedLifeSpan(`DIFFICULTY_HELLONEARTH) =22.0
    ParZedLifeSpanSolo(`DIFFICULTY_NORMAL)      =23.0
    ParZedLifeSpanSolo(`DIFFICULTY_HARD)        =23.0
    ParZedLifeSpanSolo(`DIFFICULTY_SUICIDAL)    =22.0
    ParZedLifeSpanSolo(`DIFFICULTY_HELLONEARTH) =17.0

    HighlySkilledAccuracyMod=1.25
    HighlySkilledAccuracyModMax=1.5
    LessSkilledAccuracyMod=0.75
    LessSkilledAccuracyModMin=0.5
    ZedLifeSpanHighlySkilledThreshold=0.75
    ZedLifeSpanLessSkilledThreshold=1.25
    ZedLifeSpanHighlySkilledThresholdMin=0.5
    ZedLifeSpanLessSkilledThresholdMax=1.5

    ShootingAccuracySkillWeight=0.25
    HeadShootingAccuracySkillWeight=0.75

    PerkRankPercentOfOverallSkill=0.25
    AccuracyPercentOfOverallSkill=0.25
    ZedLifeSpanPercentOfOverallSkill=0.5
    PlayersLowHealthThreshold=0.5

    LullSettings(`DIFFICULTY_NORMAL)        ={(MaxDuration=10.0, Cooldown=12.0)}
    LullSettings(`DIFFICULTY_HARD)          ={(MaxDuration=8.0, Cooldown=14.0)}
    LullSettings(`DIFFICULTY_SUICIDAL)      ={(MaxDuration=6.0, Cooldown=16.0)}
    LullSettings(`DIFFICULTY_HELLONEARTH)   ={(MaxDuration=5.0, Cooldown=18.0)}

    bLogGameConductor=false
    bBypassGameConductor=false
    GameConductorStatus=GCS_Normal
    PlayerDeathForceLullLength=15.0
    SoloPlayerSurroundedForceLullLength=10.0
}