1
0
KF2-Dev-Scripts/KFGame/Classes/KFAISpawnManager.uc

1462 lines
47 KiB
Ucode
Raw Normal View History

2020-12-13 15:01:13 +00:00
//=============================================================================
// KFAISpawnManager
//=============================================================================
// The KFAISpawnManager is created from the KFGameInfo to manage the wave
// based Zed spawning.
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Christian "schneidzekk" Schneider
//=============================================================================
class KFAISpawnManager extends Object
within KFGameInfo
config(Game)
native(AI);
`include(KFGame\KFGameAnalytics.uci);
/** Struct containing difficulty-based wave settings */
struct native DifficultyWaveInfo
{
/** All the waves*/
var array<KFAIWaveInfo> Waves;
};
/** Wave settings per difficulty */
var array<DifficultyWaveInfo> DifficultyWaveSettings;
/** The current settings for this difficulty */
var DifficultyWaveInfo WaveSettings;
/** The spawn group setup of a wave */
var array<KFAISpawnSquad> AvailableSquads;
/** Controls the frequency of the AI spawning sine wave. Used to increase/decrease speed and intensity of AI spawning */
var float SineWaveFreq;
/** WorldTime at wave start */
var float WaveStartTime;
/** Total time that combat waves have been active */
var float TotalWavesActiveTime;
/** The time the next AI group is spaning */
var float TimeUntilNextSpawn;
/** Total num AI spawned in this wave */
var int WaveTotalAI;
struct native PerPlayerMaxMonsters
{
var array<int> MaxMonsters;
};
var array<PerPlayerMaxMonsters> PerDifficultyMaxMonsters;
/** Used for arrays to modify spawn rate */
struct native SpawnRateModifier
{
var() array<float> RateModifier;
};
/** How much to modify the spawn rate for solo play by difficulty */
var() SpawnRateModifier SoloWaveSpawnRateModifier[4];
/** How much to modify the spawn rate for early waves. Generally used to make early waves more intense */
var() float EarlyWaveSpawnRateModifier[4];
/** Waves below this index will be considered "early waves" waves after this index will be considered "later waves" */
var int EarlyWaveIndex;
/** The base SpawnTimeMod to use in early waves based on the number of players playing */
var() float EarlyWavesSpawnTimeModByPlayers[6];
/** The base SpawnTimeMod to use in late waves based on the number of players playing */
var() float LateWavesSpawnTimeModByPlayers[6];
/** The number of zeds we will spawn in a whole wave is so short, the special squad may not spawn. So force it if we need to */
var bool bForceRequiredSquad;
/** Recycle the special squad every other time through the squad list */
var bool bRecycleSpecialSquad;
/** Whether to recycle the special squad every other time through the squad list, by difficulty */
var() array<bool> RecycleSpecialSquad;
/** Limits the number of times we can recycle the special squad */
var int MaxSpecialSquadRecycles;
/** The number of times we've recycled special squads this wave */
var int NumSpecialSquadRecycles;
/** Recycle the special squad every other time through the squad list */
var int NumSpawnListCycles;
/** List of available AI spawn volumes */
var array<KFSpawnVolume> SpawnVolumes;
/** Last spawn volume used by AI */
var KFSpawnVolume LastAISpawnVolume;
var ESquadType DesiredSquadType;
/** If set, can be used to spawn in place of the KF1 style volume spawns */
var KFSpawner ActiveSpawner;
/** IDs into the AIClassList array */
enum EAIType
{
AT_Clot,
AT_SlasherClot,
AT_AlphaClot,
AT_Crawler,
AT_GoreFast,
AT_Stalker,
AT_Scrake,
AT_FleshPound,
AT_FleshpoundMini,
AT_Bloat,
AT_Siren,
AT_Husk,
AT_BossRandom,
AT_EliteClot,
AT_EliteCrawler,
AT_EliteGoreFast,
AT_EDAR_EMP,
AT_EDAR_Laser,
AT_EDAR_Rocket,
};
/** IDs into the BossAIClassList array */
enum EBossAIType
{
BAT_Hans,
BAT_Patriarch,
BAT_KingFleshpound,
BAT_KingBloat,
BAT_Matriarch,
};
/** The list of zeds the spawn manager currently has queued up to spawn*/
var array< class<KFPawn_Monster> > LeftoverSpawnSquad;
/** The boss minion spawn squad we're currently spawning */
var array<KFAISpawnSquad> BossMinionsSpawnSquads;
/** Boss minions are currently being summoned */
var bool bSummoningBossMinions;
/** Maximum number of boss minions to have active at one time for this current spawning set */
var int MaxBossMinions;
/** How much to scale the number of boss minions based on the number of players playing */
var() float MaxBossMinionScaleByPlayers[6];
/** List of currently connected players. Stored here to save time going through all controllers */
var array <Controller> RecentSpawnSelectedHumanControllerList;
/* Used to force a specific boss */
var const int ForcedBossNum;
var bool bTemporarilyEndless;
2022-10-29 23:52:58 +00:00
var int VIP_CurrentSpawnCounter;
var int VIP_MaxSpawnCounter;
2020-12-13 15:01:13 +00:00
/************************************************************************************
* Debugging
***********************************************************************************/
var config bool bLogAISpawning;
var config bool bLogWaveSpawnTiming;
var config bool bLogRateVolume;
/** Builds a sorted list of spawn volumes based on distance to a specific player */
native function bool SortSpawnVolumes(Controller C, bool bTeleporting, float MinDistSquared);
2022-10-29 23:52:58 +00:00
delegate int SortVIPSpawnVolumesDelegate(KFSpawnVolume A, KFSpawnVolume B)
{
if (A.CurrentRating == B.CurrentRating)
{
return 0;
}
if (A.CurrentRating < B.CurrentRating)
{
return 1;
}
return -1;
}
function SortVIPSpawnVolumes()
{
local KFGameReplicationInfo KFGRI;
local int VolumeIndex;
if (VIP_CurrentSpawnCounter < VIP_MaxSpawnCounter)
{
++VIP_CurrentSpawnCounter;
return;
}
VIP_CurrentSpawnCounter = 0;
KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
if (KFGRI != none && KFGRI.VIPRepPlayer != none)
{
// Recalculate rating based on the distance to VIP player
for (VolumeIndex = 0; VolumeIndex < SpawnVolumes.Length; VolumeIndex++)
{
if (SpawnVolumes[VolumeIndex].CurrentRating > 0)
{
SpawnVolumes[VolumeIndex].CurrentRating = VSizeSq( SpawnVolumes[VolumeIndex].Location - KFGRI.VIPRepPlayer.Location );
}
}
}
// Sort vector based on closest
SpawnVolumes.Sort(SortVIPSpawnVolumesDelegate);
}
2020-12-13 15:01:13 +00:00
static function string ZedTypeToString(EAIType AiTypeToConvert)
{
`if(`notdefined(ShippingPC))
switch(AiTypeToConvert)
{
case AT_Clot:
return "ClotU";
case AT_SlasherClot:
return "ClotS";
case AT_AlphaClot:
return "ClotA";
case AT_Crawler:
return "Crawler";
case AT_GoreFast:
return "Gorefast";
case AT_Stalker:
return "Stalker";
case AT_Scrake:
return "Scrake";
case AT_FleshPound:
return "Fleshpound";
case AT_FleshpoundMini:
return "FleshpoundMini";
case AT_Bloat:
return "Bloat";
case AT_Siren:
return "Siren";
case AT_Husk:
return "Husk";
case AT_BossRandom:
return "Hans";
}
return "CLOT";
`endif
return "";
}
/** Let's get this party started (Called on match start) */
function Initialize()
{
`log("KFAISpawnManager.Initialize()", bLogAISpawning);
// Sets the wave settings for this difficulty
GetWaveSettings( WaveSettings );
//LoadAIClasses();
RegisterSpawnVolumes();
}
/** Adds all available spawn volumes to the list */
function RegisterSpawnVolumes()
{
local KFSpawnVolume MySpawnVolume;
SpawnVolumes.Remove(0, SpawnVolumes.Length);
foreach AllActors(class'KFSpawnVolume', MySpawnVolume)
{
// bDisabled flag used for debugging to isolate specific volumes
if( !MySpawnVolume.bDisabled )
{
SpawnVolumes.AddItem(MySpawnVolume);
}
}
`log("KFAISpawnManager.RegisterSpawnVolumes() Added" @ SpawnVolumes.Length @ "Volumes" , bLogAISpawning);
}
/** Next wave's basic setup */
function SetupNextWave(byte NextWaveIndex, int TimeToNextWaveBuffer = 0)
{
local KFGameReplicationInfo KFGRI;
2022-10-29 23:52:58 +00:00
local bool bIsBossRush;
bIsBossRush = OutbreakEvent != none && OutbreakEvent.ActiveEvent.bBossRushMode;
2020-12-13 15:01:13 +00:00
2022-10-29 23:52:58 +00:00
if (bIsBossRush)
2021-11-16 17:03:42 +00:00
{
NextWaveIndex = MyKFGRI.WaveMax - 1;
}
2020-12-13 15:01:13 +00:00
if( NextWaveIndex < WaveSettings.Waves.Length )
{
// Recycle special squads on higher difficulties
if( GameDifficulty < RecycleSpecialSquad.Length )
{
bRecycleSpecialSquad = RecycleSpecialSquad[GameDifficulty];
//`log("bRecycleSpecialSquad the special squads = "$bRecycleSpecialSquad);
}
else
{
bRecycleSpecialSquad = RecycleSpecialSquad[RecycleSpecialSquad.Length - 1];
//`log("Over array size bRecycleSpecialSquad the special squads = "$bRecycleSpecialSquad);
}
// Clear out any leftover spawn squads from last wave
LeftoverSpawnSquad.Length = 0;
// Initialize the spawn list cycles
NumSpawnListCycles=1;
// Initialize our recycle number
NumSpecialSquadRecycles = 0;
2022-10-29 23:52:58 +00:00
if (MyKFGRI.IsBossWave() || bIsBossRush)
2020-12-13 15:01:13 +00:00
{
WaveTotalAI = 1;
}
else
{
// Scale the number of zeds if the wave can be recycled
if (WaveSettings.Waves[NextWaveIndex].bRecycleWave)
{
WaveTotalAI = WaveSettings.Waves[NextWaveIndex].MaxAI *
DifficultyInfo.GetPlayerNumMaxAIModifier(GetNumHumanTeamPlayers()) *
DifficultyInfo.GetDifficultyMaxAIModifier();
}
else
{
WaveTotalAI = WaveSettings.Waves[NextWaveIndex].MaxAI;
}
WaveTotalAI *= GetTotalWaveCountScale();
WaveTotalAI = Max(1, WaveTotalAI);
}
GetAvailableSquads(NextWaveIndex, true);
WaveStartTime = WorldInfo.TimeSeconds;
TimeUntilNextSpawn = 5.f + TimeToNextWaveBuffer;
// Reset the total waves active time on first wave
if( NextWaveIndex == 0 )
{
TotalWavesActiveTime = 0;
}
KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
if( KFGRI != none && (KFGRI.bDebugSpawnManager || KFGRI.bGameConductorGraphingEnabled) )
{
KFGRI.CurrentSineMod = GetSineMod();
KFGRI.CurrentNextSpawnTime = TimeUntilNextSpawn;
KFGRI.CurrentSineWavFreq = GetSineWaveFreq();
KFGRI.CurrentNextSpawnTimeMod = GetNextSpawnTimeMod();
KFGRI.CurrentTotalWavesActiveTime = TotalWavesActiveTime;
KFGRI.CurrentMaxMonsters = GetMaxMonsters();
KFGRI.CurrentTimeTilNextSpawn = TimeUntilNextSpawn;
}
}
LastAISpawnVolume = none;
`log("KFAISpawnManager.SetupNextWave() NextWave:" @ NextWaveIndex @ "WaveTotalAI:" @ WaveTotalAI, bLogAISpawning || bLogWaveSpawnTiming);
}
/** Retrieves the wave settings for the current difficulty */
function GetWaveSettings( out DifficultyWaveInfo WaveInfo )
{
WaveInfo = DifficultyWaveSettings[Min(GameDifficulty, DifficultyWaveSettings.Length-1)];
}
function GetAvailableSquads(byte MyWaveIndex, optional bool bNeedsSpecialSquad=false)
{
local int i, j, TotalZedsInSquads;
if( WaveSettings.Waves[MyWaveIndex] != none )
{
NumSpawnListCycles++;
WaveSettings.Waves[MyWaveIndex].GetNewSquadList(AvailableSquads);
`log("KFAISpawnManager NEW SQUAD LIST for Wave:" @ MyWaveIndex, bLogAISpawning);
if( bNeedsSpecialSquad )
{
WaveSettings.Waves[MyWaveIndex].GetSpecialSquad(AvailableSquads);
`log("KFAISpawnManager SPECIAL SQUAD for Wave:" @ MyWaveIndex, bLogAISpawning);
for( i = 0; i < AvailableSquads.Length; i++ )
{
for( j = 0; j < AvailableSquads[i].MonsterList.Length; j++ )
{
TotalZedsInSquads += AvailableSquads[i].MonsterList[j].Num;
}
}
//`log("WaveTotalAI :"$WaveTotalAI$" TotalZedsInSquads = "$TotalZedsInSquads);
if( WaveTotalAI < TotalZedsInSquads )
{
bForceRequiredSquad = true;
}
}
}
}
/** Spawns in our remaining reserved zeds */
function SpawnRemainingReservedZeds( optional bool bSpawnAllReservedZeds );
/** When we recycle a squad/have a leftover squad, make sure it knows what size
* Volume it can spawn in. If for instance a larger zed has been removed from
* the list, now the rest of the zeds may be able to spawn elsewhere. That or
* the DesiredSquadType might have been initialized for a different size of
* Zed groups, and be completely invalid and unset for this leftover group
*/
function ESquadType GetDesiredSquadTypeForZedList( array< class<KFPawn_Monster> > NewSquad )
{
local int i;
local ESquadType LargestMonsterSquadType;
// Start with the smallest size, and the crank it up if the squad is larger
LargestMonsterSquadType = EST_Crawler;
for ( i = 0; i < NewSquad.Length; i++ )
{
`log(GetFuncName()$" LeftOverSquad "$NewSquad[i]$" MinSquadType:"$ GetEnum(enum'ESquadType',NewSquad[i].default.MinSpawnSquadSizeType),bLogAISpawning);
if( NewSquad[i].default.MinSpawnSquadSizeType < LargestMonsterSquadType )
{
LargestMonsterSquadType = NewSquad[i].default.MinSpawnSquadSizeType;
}
}
return LargestMonsterSquadType;
}
function SetDesiredSquadTypeForZedList( array< class<KFPawn_Monster> > NewSquad )
{
DesiredSquadType = GetDesiredSquadTypeForZedList( NewSquad );
`log(GetFuncName()$" adjusted largest squad for squad leftover squad to "$GetEnum(enum'ESquadType',DesiredSquadType),bLogAISpawning);
}
/** generate a list of classes to spawn for a given squad */
function GetSpawnListFromSquad(byte SquadIdx, out array< KFAISpawnSquad > SquadsList, out array< class<KFPawn_Monster> > AISpawnList)
{
local KFAISpawnSquad Squad;
local EAIType AIType;
local int i, j, RandNum;
local ESquadType LargestMonsterSquadType;
local array<class<KFPawn_Monster> > TempSpawnList;
local class<KFPawn_Monster> ForcedPawnClass;
2021-11-16 17:03:42 +00:00
local int RandBossIndex;
2020-12-13 15:01:13 +00:00
Squad = SquadsList[SquadIdx];
// Start with the smallest size, and the crank it up if the squad is larger
LargestMonsterSquadType = EST_Crawler;
for ( i = 0; i < Squad.MonsterList.Length; i++ )
{
for ( j = 0; j < Squad.MonsterList[i].Num; j++ )
{
if( Squad.MonsterList[i].CustomClass != None )
{
TempSpawnList.AddItem(Squad.MonsterList[i].CustomClass);
}
else
{
AIType = Squad.MonsterList[i].Type;
if( AIType == AT_BossRandom )
{
`if(`notdefined(ShippingPC))
if( ForcedBossNum >= 0 )
{
if (ForcedBossNum < AIBossClassList.Length)
{
ForcedPawnClass = AIBossClassList[ForcedBossNum];
}
else if ((ForcedBossNum - AIBossClassList.Length) < AITestBossClassList.Length)
{
ForcedPawnClass = AITestBossClassList[ForcedBossNum - AIBossClassList.Length];
}
//If we've forced the class we don't greatly care about the memory results, preload content now
ForcedPawnClass.static.PreloadContent();
TempSpawnList.AddItem(ForcedPawnClass);
}
else
`endif
//Always have the squad type be a boss if we're spawning one in case of override
2021-11-16 17:03:42 +00:00
if (OutbreakEvent.ActiveEvent.bBossRushMode)
{
RandBossIndex = Rand(BossRushEnemies.length);
TempSpawnList.AddItem( default.AIBossClassList[BossRushEnemies[RandBossIndex]]);
BossRushEnemies.Remove(RandBossIndex, 1);
}
else
{
TempSpawnList.AddItem(GetBossAISpawnType());
}
2020-12-13 15:01:13 +00:00
LargestMonsterSquadType = EST_Boss;
}
else
{
TempSpawnList.AddItem(GetAISpawnType(AIType));
}
}
if( TempSpawnList[TempSpawnList.Length - 1].default.MinSpawnSquadSizeType < LargestMonsterSquadType )
{
LargestMonsterSquadType = TempSpawnList[TempSpawnList.Length - 1].default.MinSpawnSquadSizeType;
}
}
}
if( TempSpawnList.Length > 0 )
{
// Shuffle spawn list
while( TempSpawnList.Length > 0 )
{
RandNum = Rand( TempSpawnList.Length );
AISpawnList.AddItem( TempSpawnList[RandNum] );
TempSpawnList.Remove( RandNum, 1 );
}
DesiredSquadType = Squad.MinVolumeType;
if( LargestMonsterSquadType < DesiredSquadType )
{
DesiredSquadType = LargestMonsterSquadType;
//`log("adjusted largest squad for squad "$Squad$" to "$GetEnum(enum'ESquadType',DesiredSquadType));
}
}
}
/** Returns a random AIGroup from the "waiting" list */
function array< class<KFPawn_Monster> > GetNextSpawnList()
{
local array< class<KFPawn_Monster> > NewSquad, RequiredSquad;
local int RandNum, AINeeded;
if( LeftoverSpawnSquad.Length > 0 )
{
if( bLogAISpawning )
{
`log("KFAISpawnManager.GetNextSpawnList - LeftoverSpawnSquad.Length > 0", bLogAISpawning);
LogMonsterList(LeftoverSpawnSquad, "Leftover LeftoverSpawnSquad");
}
NewSquad = LeftoverSpawnSquad;
// Make sure we properly initialize the DesiredSquadType for the leftover squads, otherwise they will just use whatever size data was left in the system
SetDesiredSquadTypeForZedList( NewSquad );
}
else
{
// Get a new monster list
if( !IsAISquadAvailable() )
{
`log("KFAISpawnManager.GetNextSpawnList - No AI Squad available - bSummoningBossMinions:" @ bSummoningBossMinions, bLogAISpawning);
if( !bSummoningBossMinions )
{
// WaveNum Displays 1 - Length, Squads are ordered 0 - (Length - 1)
if( bRecycleSpecialSquad && NumSpawnListCycles % 2 == 1 && (MaxSpecialSquadRecycles == -1 || NumSpecialSquadRecycles < MaxSpecialSquadRecycles) )
{
//`log("Recycling special squad!!! NumSpawnListCycles: "$NumSpawnListCycles);
GetAvailableSquads(MyKFGRI.WaveNum - 1, true);
++NumSpecialSquadRecycles;
}
else
{
//`log("Not recycling special squad!!! NumSpawnListCycles: "$NumSpawnListCycles);
GetAvailableSquads(MyKFGRI.WaveNum - 1);
}
}
else
{
// Replace the regular squads with boss minions
AvailableSquads = BossMinionsSpawnSquads;
}
}
// select a random squad from the list
RandNum = Rand(AvailableSquads.Length);
// If we're forcing the required squad, and it already got picked, clear the flag
if( bForceRequiredSquad && RandNum == (AvailableSquads.Length - 1) )
{
//`log("We spawned the required squad!");
bForceRequiredSquad=false;
}
if( bLogAISpawning )
{
LogAvailableSquads();
}
`log("KFAISpawnManager.GetNextAIGroup() Wave:" @ MyKFGRI.WaveNum @ "Squad:" @ AvailableSquads[RandNum] @ "Index:" @ RandNum, bLogAISpawning);
// generate list of classes to spawn
GetSpawnListFromSquad(RandNum, AvailableSquads, NewSquad);
// Grab the required squad (special squad) which will be the last squad in the array,
// if we're about to run out of zeds we can spawn, and the special squad hasn't spawned yet
if( bForceRequiredSquad )
{
// generate list of classes to spawn
GetSpawnListFromSquad((AvailableSquads.Length - 1), AvailableSquads, RequiredSquad);
if( (NumAISpawnsQueued + NewSquad.Length + RequiredSquad.Length) > WaveTotalAI )
{
NewSquad = RequiredSquad;
RandNum = (AvailableSquads.Length - 1);
//LogMonsterList(NewSquad, "RequiredSquad");
//`log("Spawning required squad NumAISpawnsQueued: "$NumAISpawnsQueued$" NewSquad.Length: "$NewSquad.Length$" RequiredSquad.Length: "$RequiredSquad.Length$" WaveTotalAI: "$WaveTotalAI);
bForceRequiredSquad=false;
}
}
// remove selected squad from the list of available squads
AvailableSquads.Remove(RandNum, 1);
if( bLogAISpawning )
{
LogAvailableSquads();
}
}
// Clamp list by NumAINeeded()
AINeeded = GetNumAINeeded();
if( AINeeded < NewSquad.Length )
{
LeftoverSpawnSquad = NewSquad;
// Clear out the monsters we're about to spawn from the leftover list
LeftoverSpawnSquad.Remove( 0, AINeeded );
// Cut off the leftovers from the new monster list
NewSquad.Length = AINeeded;
}
else
{
// If we're spawning all the monsters in the list, there are no leftovers
LeftoverSpawnSquad.Length = 0;
}
if( bLogAISpawning )
{
LogMonsterList( NewSquad, "NewSquad" );
LogMonsterList( LeftoverSpawnSquad, "LeftoverSpawnSquad" );
}
return NewSquad;
}
/** Print out a list of monsters */
function LogMonsterList(array< class<KFPawn_Monster> > MonsterList, String ListName)
{
local int i;
`log("Monster List "$ListName$" Length = "$MonsterList.Length);
for( i = 0; i < MonsterList.Length; i++ )
{
`log( "MonsterList "$ListName$" element "$i$" is "$MonsterList[i] );
}
}
/** Print out the available squad list */
function LogAvailableSquads()
{
local int i, j;
`log("Current Loaded Wave Setup");
for( i = 0; i < AvailableSquads.Length; i++ )
{
for( j = 0; j < AvailableSquads[i].MonsterList.Length; j++ )
{
`log( "AvailableSquads "$i$" MonsterList "$j$" is "$GetEnum(enum'EAIType',SpawnManager.AvailableSquads[i].MonsterList[j].Type)$" Num: "$SpawnManager.AvailableSquads[i].MonsterList[j].Num$" SquadName: "$AvailableSquads[i] );
}
}
}
/** Another spawn group avaiblale */
function bool IsAISquadAvailable()
{
`log("KFAISpawnManager.IsAISquadAvailable()" @ string(AvailableSquads.Length > 0), bLogAISpawning);
return ( AvailableSquads.Length > 0 );
}
/** Any AI left for this wave */
function bool IsFinishedSpawning()
{
if( ActiveSpawner != none && ActiveSpawner.bIsSpawning && ActiveSpawner.PendingSpawns.Length > 0 )
{
`log("KFAISpawnManager.IsFinishedSpawning() ActiveSpawner Still Spawning " @ string(ActiveSpawner != none && ActiveSpawner.bIsSpawning && ActiveSpawner.PendingSpawns.Length > 0), bLogAISpawning);
return false;
}
// Keep spawning the boss minions until we tell it to stop!
if( bSummoningBossMinions )
{
`log("KFAISpawnManager.IsFinishedSpawning() Summoning Boss Minions.", bLogAISpawning);
return false;
}
if (bTemporarilyEndless)
{
`log("KFAISpawnManager.IsFinishedSpawning() Temporarily Endless", bLogAISpawning);
return false;
}
if( NumAISpawnsQueued >= WaveTotalAI )
{
`log("KFAISpawnManager.IsFinishedSpawning()" @ string(NumAISpawnsQueued >= WaveTotalAI), bLogAISpawning);
return true;
}
return false;
}
/**
* Returns the next AI spawn time
*/
function float CalcNextGroupSpawnTime()
{
local float NextSpawnDelay, SineMod;
local KFMapInfo KFMI;
local KFGameReplicationInfo KFGRI;
// Any leftover zeds from a group that didn't spawn, spawn them right away!
if( LeftoverSpawnSquad.Length > 0 )
{
return 0;
}
else
{
KFMI = KFMapInfo(WorldInfo.GetMapInfo());
SineMod = GetSineMod();
NextSpawnDelay = KFMI != none ? KFMI.WaveSpawnPeriod : class'KFMapInfo'.default.WaveSpawnPeriod;
NextSpawnDelay *= GetNextSpawnTimeMod();
NextSpawnDelay += SineMod * (NextSpawnDelay * 2);
KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
if( KFGRI != none && (KFGRI.bDebugSpawnManager || KFGRI.bGameConductorGraphingEnabled) )
{
KFGRI.CurrentSineMod = SineMod;
KFGRI.CurrentNextSpawnTime = NextSpawnDelay;
KFGRI.CurrentSineWavFreq = GetSineWaveFreq();
KFGRI.CurrentNextSpawnTimeMod = GetNextSpawnTimeMod();
}
}
`log(GetFuncName()$" NextSpawnTime:" @ WorldInfo.TimeSeconds + NextSpawnDelay @"NextSpawnDelay:"@NextSpawnDelay$" SineMod: "$SineMod$" WaveSpawnPeriod: "$(KFMI != none ? KFMI.WaveSpawnPeriod : class'KFMapInfo'.default.WaveSpawnPeriod)$" GetNextSpawnTimeMod(): "$GetNextSpawnTimeMod(), bLogAISpawning || bLogWaveSpawnTiming);
return NextSpawnDelay;
}
/**
* Returns a modifer based on wave and player number
* to adjust the next spawn time
*/
function float GetNextSpawnTimeMod()
{
local byte NumLivingPlayers;
local float SpawnTimeMod;
local float UsedEarlyWaveRateMod;
local float UsedSoloWaveRateMod;
NumLivingPlayers = GetLivingPlayerCount();
SpawnTimeMod = 1.0;
UsedSoloWaveRateMod = 1.0;
// Scale solo spawning rate by wave and difficulty
if( bOnePlayerAtStart && NumLivingPlayers <= 1 )
{
if( GameDifficulty < ArrayCount(SoloWaveSpawnRateModifier) )
{
if( MyKFGRI.WaveNum <= SoloWaveSpawnRateModifier[GameDifficulty].RateModifier.Length )
{
UsedSoloWaveRateMod = SoloWaveSpawnRateModifier[GameDifficulty].RateModifier[MyKFGRI.WaveNum - 1];
}
else
{
UsedSoloWaveRateMod = SoloWaveSpawnRateModifier[GameDifficulty].RateModifier[SoloWaveSpawnRateModifier[GameDifficulty].RateModifier.Length -1];
}
}
else
{
if( MyKFGRI.WaveNum <= SoloWaveSpawnRateModifier[GameDifficulty].RateModifier.Length )
{
UsedSoloWaveRateMod = SoloWaveSpawnRateModifier[ArrayCount(SoloWaveSpawnRateModifier) - 1].RateModifier[MyKFGRI.WaveNum - 1];
}
else
{
UsedSoloWaveRateMod = SoloWaveSpawnRateModifier[ArrayCount(SoloWaveSpawnRateModifier) - 1].RateModifier[SoloWaveSpawnRateModifier[GameDifficulty].RateModifier.Length -1];
}
}
}
if ( MyKFGRI.WaveNum < EarlyWaveIndex )
{
// Set the base SpawnTimeMod for early waves based on the number of living players
if( NumLivingPlayers <= ArrayCount(EarlyWavesSpawnTimeModByPlayers) )
{
if( NumLivingPlayers == 0 )
{
SpawnTimeMod = EarlyWavesSpawnTimeModByPlayers[NumLivingPlayers];
}
else
{
SpawnTimeMod = EarlyWavesSpawnTimeModByPlayers[NumLivingPlayers - 1];
}
}
else
{
SpawnTimeMod = EarlyWavesSpawnTimeModByPlayers[ArrayCount(EarlyWavesSpawnTimeModByPlayers) - 1];
}
`log("Early Waves SpawnTimeMod = "$SpawnTimeMod$" NumLivingPlayers = "$NumLivingPlayers$" UsedSoloWaveRateMod = "$UsedSoloWaveRateMod, bLogAISpawning);
// Scale the spawning rate of early waves by difficulty (generally to make them more intense)
if( GameDifficulty < ArrayCount(EarlyWaveSpawnRateModifier) )
{
UsedEarlyWaveRateMod = EarlyWaveSpawnRateModifier[GameDifficulty];
}
else
{
UsedEarlyWaveRateMod = EarlyWaveSpawnRateModifier[ArrayCount(EarlyWaveSpawnRateModifier) - 1];
}
SpawnTimeMod *= UsedEarlyWaveRateMod;
SpawnTimeMod *= UsedSoloWaveRateMod;
`log("Early waves final SpawnTimeMod = "$SpawnTimeMod, bLogAISpawning);
}
// Give a slightly bigger breather in the later waves
else
{
// Set the base SpawnTimeMod for late waves based on the number of living players
if( NumLivingPlayers <= ArrayCount(LateWavesSpawnTimeModByPlayers) )
{
if( NumLivingPlayers == 0 )
{
SpawnTimeMod = LateWavesSpawnTimeModByPlayers[NumLivingPlayers];
}
else
{
SpawnTimeMod = LateWavesSpawnTimeModByPlayers[NumLivingPlayers - 1];
}
}
else
{
SpawnTimeMod = LateWavesSpawnTimeModByPlayers[ArrayCount(LateWavesSpawnTimeModByPlayers) - 1];
}
`log("Late waves SpawnTimeMod = "$SpawnTimeMod$" NumLivingPlayers = "$NumLivingPlayers$" UsedSoloWaveRateMod = "$UsedSoloWaveRateMod, bLogAISpawning);
SpawnTimeMod *= UsedSoloWaveRateMod;
`log("Late waves final SpawnTimeMod = "$SpawnTimeMod, bLogAISpawning);
}
// Apply difficulty based modifier
if ( DifficultyInfo != None )
{
SpawnTimeMod *= DifficultyInfo.GetSpawnRateModifier();
`log("Spawn rate modifier (difficulty):" @ DifficultyInfo.GetSpawnRateModifier(), bLogAISpawning);
}
//Apply global game mode spawn rate modifier
SpawnTimeMod *= GetGameInfoSpawnRateMod() * GameConductor.CurrentSpawnRateModification;
return Max(SpawnTimeMod, 0.f);
}
/** Modify the next spawn time based on a sine wave value */
function float GetSineMod()
{
return 1.0 - Abs(sin( TotalWavesActiveTime * GetSineWaveFreq() ));
}
/** Returns the current spawn intensity */
function float GetSineWaveFreq()
{
return SineWaveFreq;
}
/** Objectives can change the spawn intensity */
function SetSineWaveFreq(float NewFreq)
{
SineWaveFreq = NewFreq;
}
/** special spawning for boss summon ability */
function SummonBossMinions( array<KFAISpawnSquad> NewMinionSquad, int NewMaxBossMinions, optional bool bUseLivingPlayerScale = true )
{
local int NumLivePlayers;
local float UsedMaxBossMinionsScale;
// Clear out the previous boss minion spawning before adding new minions
if( bSummoningBossMinions )
{
StopSummoningBossMinions();
}
bSummoningBossMinions=true;
// Store the squad that were spawning with the boss
BossMinionsSpawnSquads = NewMinionSquad;
AvailableSquads = BossMinionsSpawnSquads;
// Calculate how many minions to spawn basedoff the base
MaxBossMinions = NewMaxBossMinions;
NumLivePlayers = GetLivingPlayerCount();
// Scale boss minions numbers by player count
if (bUseLivingPlayerScale)
{
if (NumLivePlayers <= ArrayCount(MaxBossMinionScaleByPlayers))
{
if (NumLivePlayers == 0)
{
UsedMaxBossMinionsScale = MaxBossMinionScaleByPlayers[NumLivePlayers];
}
else
{
UsedMaxBossMinionsScale = MaxBossMinionScaleByPlayers[NumLivePlayers - 1];
}
}
else
{
UsedMaxBossMinionsScale = MaxBossMinionScaleByPlayers[ArrayCount(MaxBossMinionScaleByPlayers) - 1];
}
}
else
{
UsedMaxBossMinionsScale = 1.f;
}
MaxBossMinions *= UsedMaxBossMinionsScale;
// Don't let us get more Boss minions than we allow total monsters
MaxBossMinions = Min(MaxBossMinions, GetMaxMonsters());
`log(self@GetFuncName()$" MaxBossMinions:"$MaxBossMinions, bLogAISpawning);
}
/** Stop spawning boss minions */
function StopSummoningBossMinions()
{
bSummoningBossMinions=false;
// Clear out the boss minions we added to the AvailableSquads
AvailableSquads.Length=0;
}
/** Find best spawn location and spawn a squad there */
function int SpawnSquad( out array< class<KFPawn_Monster> > AIToSpawn, optional bool bSkipHumanZedSpawning=false )
{
local KFSpawnVolume KFSV;
local int SpawnerAmount, VolumeAmount, FinalAmount, i;
local bool bCanSpawnPlayerBoss;
`if(`notdefined(ShippingPC))
local KFGameReplicationInfo KFGRI;
local vector VolumeLocation;
`endif
// Since this is called from multiple locations, early out if we're not in a wave
if( !IsWaveActive() )
{
return 0;
}
// first check scripted spawners
if( ActiveSpawner != None && ActiveSpawner.CanSpawnHere(DesiredSquadType) )
{
SpawnerAmount = ActiveSpawner.SpawnSquad(AIToSpawn);
`log("KFAISpawnManager.SpawnAI() Using Spawner AIs spawned:" @ SpawnerAmount @ "in Spawner:" @ ActiveSpawner, bLogAISpawning);
}
// otherwise use default spawn volume selection
if( AIToSpawn.Length > 0 )
{
KFSV = GetBestSpawnVolume(AIToSpawn);
if( KFSV != None )
{
`if(`notdefined(ShippingPC))
VolumeLocation=KFSV.Location;
`endif
KFSV.VolumeChosenCount++;
if( bLogAISpawning )
{
LogMonsterList(AIToSpawn, "SpawnSquad Pre Spawning");
}
bCanSpawnPlayerBoss = (bIsVersusGame && MyKFGRI.WaveNum == MyKFGRI.WaveMax) ? CanSpawnPlayerBoss() : false;
if( !bIsVersusGame || MyKFGRI.WaveNum < MyKFGRI.WaveMax || !bCanSpawnPlayerBoss )
{
VolumeAmount = KFSV.SpawnWave(AIToSpawn, true);
LastAISpawnVolume = KFSV;
}
if( bIsVersusGame && !bSkipHumanZedSpawning && MyKFGRI.WaveNum == MyKFGRI.WaveMax )
{
AIToSpawn.Length = 0;
}
`log("KFAISpawnManager.SpawnAI() AIs spawned:" @ VolumeAmount @ "in Volume:" @ KFSV, bLogAISpawning);
if( bLogAISpawning )
{
LogMonsterList(AIToSpawn, "SpawnSquad Post Spawning");
}
`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(KFSV.Location);
}
`endif
}
if( VolumeAmount == 0 )
{
// `warn(self@GetFuncName()$" No spawn volume with a positive rating, no AI will spawn!!!");
}
}
FinalAmount = VolumeAmount + SpawnerAmount;
NumAIFinishedSpawning += VolumeAmount; // volume zeds get spawned right away, so add this here
RefreshMonsterAliveCount();
if( AIToSpawn.Length > 0 )
{
//`warn(self@GetFuncName()$" Didn't spawn the whole list of AI!!!");
`if(`notdefined(ShippingPC))
// Let the GRI know that a spawn volume failed to spawn some AI
if( !IsZero(VolumeLocation) )
{
KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
if( KFGRI != none && KFGRI.bTrackingMapEnabled )
{
KFGRI.AddFailedSpawn(VolumeLocation);
}
}
`endif
if( bLogAISpawning )
{
LogMonsterList(AIToSpawn, "SpawnSquad Incomplete Spawn Remaining");
LogMonsterList(LeftoverSpawnSquad, "Failed Spawn Before Adding To Leftovers");
}
// Add any failed spawns back into the LeftoverSpawnSquad to rapidly spawn somewhere else
for ( i = 0; i < AIToSpawn.Length; i++ )
{
LeftoverSpawnSquad[LeftoverSpawnSquad.Length] = AIToSpawn[i];
}
if( bLogAISpawning )
{
LogMonsterList(LeftoverSpawnSquad, "Failed Spawn After Adding To Leftovers");
}
}
/* __TW_ANALYTICS_ */
if( bEnableGameAnalytics )
RecordSpawnInformation( KFSV, FinalAmount );
return FinalAmount;
}
/**
* Records AI debugging and tuning info for spawning
*/
/* __TW_ANALYTICS_ */
function RecordSpawnInformation( KFSpawnVolume BestSpawnVolume, int ZedsSpawned )
{
local KFPlayerController KFPC;
local vector ViewLoc;
local rotator ViewRot;
foreach WorldInfo.AllControllers(class'KFPlayerController', KFPC)
{
if( KFPC.Pawn == none || !KFPC.Pawn.IsAliveAndWell() )
{
continue;
}
ViewLoc = KFPC.Pawn.GetPawnViewLocation();
ViewRot = KFPC.Pawn.GetViewRotation();
`RecordPlayersAtSpawn( KFPC, ViewLoc, ViewRot );
}
`RecordBestSpawnVolume( BestSpawnVolume, MyKFGRI.WaveNum, ZedsSpawned );
}
/** "Timer" */
function Update()
{
local array<class<KFPawn_Monster> > SpawnList;
if( IsWaveActive() )
{
TotalWavesActiveTime += 1.0;
TimeUntilNextSpawn -= 1.f;
if( ShouldAddAI() )
{
SpawnList = GetNextSpawnList();
NumAISpawnsQueued += SpawnSquad( SpawnList );
TimeUntilNextSpawn = CalcNextGroupSpawnTime();
UpdateAIRemaining();
}
}
}
/** Should we? */
function bool ShouldAddAI()
{
//`log("KFAISpawnManager.ShouldAddAI()" @ string( (TimeUntilNextSpawn <= 0s && IsAIAvailable()) && (GetMaxMonsters() > AIAliveCount) ), bLogAISpawning);
// If it is time to spawn the next squad, or there are any leftovers from the last batch spawn them
if( (LeftoverSpawnSquad.Length > 0 || TimeUntilNextSpawn <= 0) && !IsFinishedSpawning() )
{
return GetNumAINeeded() > 0;
}
return false;
}
function int GetMaxMonsters()
{
local int LivingPlayerCount;
local int Difficulty;
LivingPlayerCount = Clamp(GetLivingPlayerCount() - 1, 0, 5);
Difficulty = Clamp(GameDifficulty, 0, 3);
return PerDifficultyMaxMonsters[Difficulty].MaxMonsters[LivingPlayerCount];
}
/** Max currently wanted AI num */
function int GetNumAINeeded()
{
local int AINeeded;
local int UsedMaxMonsters;
local KFPawn_Monster ActiveBoss;
if (bSummoningBossMinions)
{
if (AIDirector != none)
{
ActiveBoss = AIDirector.GetActiveBoss();
if (ActiveBoss == none || ActiveBoss.bPlayedDeath)
{
return 0; // Boss is dead, don't spawn any more minions.
}
}
UsedMaxMonsters = MaxBossMinions + 1; // Add 1 to include the Boss himself
}
else if (bTemporarilyEndless)
{
UsedMaxMonsters = GetMaxMonsters();
}
else
{
if (WaveTotalAI - NumAISpawnsQueued <= 0)
{
return 0;
}
UsedMaxMonsters = GetMaxMonsters();
}
if( ActiveSpawner != none && ActiveSpawner.bIsSpawning && ActiveSpawner.PendingSpawns.Length > 0 )
{
// Don't add extra AI if we've queued up AI to spawn at a spawner
AINeeded = (UsedMaxMonsters - ActiveSpawner.PendingSpawns.Length) - AIAliveCount;
`log("KFAISpawnManager.GetNumAINeeded() ActiveSpawner.PendingSpawns.Length:" @ ActiveSpawner.PendingSpawns.Length, bLogAISpawning);
}
else
{
AINeeded = UsedMaxMonsters - GetAIAliveCount();
}
if( !bSummoningBossMinions && !bTemporarilyEndless )
{
if( AINeeded > WaveTotalAI )
{
AINeeded = WaveTotalAI;
}
if( AINeeded > WaveTotalAI - NumAISpawnsQueued )
{
AINeeded = WaveTotalAI - NumAISpawnsQueued;
}
}
`log("KFAISpawnManager.GetNumAINeeded() WaveTotalAI:" @ WaveTotalAI @ "AIAliveCount:" @ AIAliveCount @ "NumAISpawnsQueued:" @ NumAISpawnsQueued @ "AINeeded:" @ AINeeded, bLogAISpawning);
return AINeeded;
}
/** Returns the number of alive AI */
function int GetAIAliveCount()
{
return AIAliveCount;
}
/** Initialize the list of Human team Controllers to use for spawn selection*/
function InitControllerList()
{
local int i;
local Controller C;
// Validate the existing list
for( i = RecentSpawnSelectedHumanControllerList.Length - 1; i >= 0; i-- )
{
// Must not be null, have a pawn, be alive, and be on the right team
if( RecentSpawnSelectedHumanControllerList[i] == none || RecentSpawnSelectedHumanControllerList[i].Pawn == none
|| !RecentSpawnSelectedHumanControllerList[i].Pawn.IsAliveAndWell()
|| RecentSpawnSelectedHumanControllerList[i].GetTeamNum() != 0 )
{
if( RecentSpawnSelectedHumanControllerList[i] != none && RecentSpawnSelectedHumanControllerList[i].PlayerReplicationInfo != none )
{
`Log( GetFuncName()$" Removing "$RecentSpawnSelectedHumanControllerList[i].PlayerReplicationInfo.PlayerName$" From HumanPRIList", bLogAISpawning );
}
else
{
`Log( GetFuncName()$" Removing empty Controller From HumanPRIList", bLogAISpawning );
}
RecentSpawnSelectedHumanControllerList.Remove( i, 1 );
}
}
if( MyKFGRI != none )
{
if( RecentSpawnSelectedHumanControllerList.Length == 0 )
{
for ( i = 0; i < MyKFGRI.PriArray.Length; i++ )
{
C = Controller(MyKFGRI.PriArray[i].Owner);
// If not a valid controller, on to the next one
if( C == none || C.Pawn == none || !C.Pawn.IsAliveAndWell() || C.GetTeamNum() != 0 )
{
continue;
}
RecentSpawnSelectedHumanControllerList[RecentSpawnSelectedHumanControllerList.Length] = C;
`Log( GetFuncName()$" Adding "$RecentSpawnSelectedHumanControllerList[RecentSpawnSelectedHumanControllerList.Length - 1].PlayerReplicationInfo.PlayerName$" to RecentSpawnSelectedHumanControllerList", bLogAISpawning );
}
}
}
}
function KFSpawnVolume GetBestSpawnVolume( optional array< class<KFPawn_Monster> > AIToSpawn, optional Controller OverrideController, optional Controller OtherController, optional bool bTeleporting, optional float MinDistSquared )
{
local int VolumeIndex, ControllerIndex;
local Controller RateController;
2022-10-29 23:52:58 +00:00
local KFGameReplicationInfo KFGRI;
2020-12-13 15:01:13 +00:00
if( OverrideController != none )
{
RateController = OverrideController;
}
else
{
// Get the Controller list ready for spawn selection
InitControllerList();
if( RecentSpawnSelectedHumanControllerList.Length > 0 )
{
// Randomly grab a Human PRI from the list to use for rating zed spawning
ControllerIndex = Rand(RecentSpawnSelectedHumanControllerList.Length);
RateController = RecentSpawnSelectedHumanControllerList[ControllerIndex];
RecentSpawnSelectedHumanControllerList.Remove( ControllerIndex, 1 );
`Log( GetFuncName()$" Rating with Controller "$RateController.PlayerReplicationInfo.PlayerName$" From RecentSpawnSelectedHumanControllerList", bLogAISpawning );
}
}
// If there were no controllers to rate against, return none
if( RateController == none )
{
`warn( GetFuncName()$" no controllers to rate spawning with!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", bLogAISpawning);
return none;
}
if( (OtherController == none || !OtherController.bIsPlayer) && NeedPlayerSpawnVolume() )
{
// Grab the first player controller
foreach WorldInfo.AllControllers( class'Controller', OtherController )
{
if( OtherController.bIsPlayer )
{
break;
}
}
}
// pre-sort the list to reduce the number of line checks performed by IsValidForSpawn
SortSpawnVolumes(RateController, bTeleporting, MinDistSquared);
2022-10-29 23:52:58 +00:00
KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
if (KFGRI != none && KFGRI.IsVIPMode())
{
SortVIPSpawnVolumes();
}
2020-12-13 15:01:13 +00:00
for ( VolumeIndex = 0; VolumeIndex < SpawnVolumes.Length; VolumeIndex++ )
{
if ( SpawnVolumes[VolumeIndex].IsValidForSpawn(DesiredSquadType, OtherController)
&& SpawnVolumes[VolumeIndex].CurrentRating > 0 )
{
`log(GetFuncName()@"returning chosen spawn volume"@SpawnVolumes[VolumeIndex]@"with a rating of"@SpawnVolumes[VolumeIndex].CurrentRating, bLogAISpawning);
return SpawnVolumes[VolumeIndex];
}
}
//`warn(GetFuncName()$" No spawn volume with a positive rating!!!");
return none;
}
function bool NeedPlayerSpawnVolume();
function UpdateSpawnCurveIntensity(float NewSineFreq)
{
if( NewSineFreq != 0 )
{
SetSineWaveFreq(NewSineFreq);
}
}
function ResetSpawnCurveIntensity()
{
SetSineWaveFreq(default.SineWaveFreq);
}
/** Determines whether we can spawn a player boss or not */
protected function bool CanSpawnPlayerBoss();
/** Can be called to reset values within the spawn manager, without using explicit outer */
function ResetSpawnManager();
defaultproperties
{
ForcedBossNum=-1
SineWaveFreq=0.04
EarlyWaveIndex=7
//This is a list of per-player count per-difficulty max monsters, starting with
//Normal
PerDifficultyMaxMonsters[0]={(
MaxMonsters[0]=10,
MaxMonsters[1]=14,
MaxMonsters[2]=32,
MaxMonsters[3]=32,
MaxMonsters[4]=32,
MaxMonsters[5]=32
)}
//Hard
PerDifficultyMaxMonsters[1]={(
MaxMonsters[0]=11,
MaxMonsters[1]=18,
MaxMonsters[2]=32,
MaxMonsters[3]=32,
MaxMonsters[4]=32,
MaxMonsters[5]=32
)}
//Suicidial
PerDifficultyMaxMonsters[2]={(
MaxMonsters[0]=12,
MaxMonsters[1]=18,
MaxMonsters[2]=32,
MaxMonsters[3]=32,
MaxMonsters[4]=32,
MaxMonsters[5]=32
)}
//Hell on Earth
PerDifficultyMaxMonsters[3]={(
MaxMonsters[0]=12, //16
MaxMonsters[1]=18,
MaxMonsters[2]=32, //22
MaxMonsters[3]=32, //26
MaxMonsters[4]=32, //30
MaxMonsters[5]=32
)}
// Normal
SoloWaveSpawnRateModifier(0)={(RateModifier[0]=1.0, // Wave 1
RateModifier[1]=1.0, // Wave 2
RateModifier[2]=1.0, // Wave 3
RateModifier[3]=1.0)} // Wave 4
// Hard
SoloWaveSpawnRateModifier(1)={(RateModifier[0]=1.0, // Wave 1
RateModifier[1]=1.0, // Wave 2
RateModifier[2]=1.0, // Wave 3
RateModifier[3]=1.0)} // Wave 4
// Suicidal
SoloWaveSpawnRateModifier(2)={(RateModifier[0]=1.0, // Wave 1
RateModifier[1]=1.0, // Wave 2
RateModifier[2]=1.0, // Wave 3
RateModifier[3]=1.0)} // Wave 4
// Hell On Earth
SoloWaveSpawnRateModifier(3)={(RateModifier[0]=1.0, // Wave 1
RateModifier[1]=1.0, // Wave 2
RateModifier[2]=1.0, // Wave 3
RateModifier[3]=1.0)} // Wave 4
EarlyWavesSpawnTimeModByPlayers(0)=1.0 // 1 player
EarlyWavesSpawnTimeModByPlayers(1)=1.3 // 2 players //1.0 //1.38 //1.0--Aug15th
EarlyWavesSpawnTimeModByPlayers(2)=0.9 // 3 players //1.0 //1.11
EarlyWavesSpawnTimeModByPlayers(3)=0.7 // 4 players //0.85 //0.84
EarlyWavesSpawnTimeModByPlayers(4)=0.4 // 5 players //0.65 //0.57 //0.5--Aug15th
EarlyWavesSpawnTimeModByPlayers(5)=0.3 // 6 players
LateWavesSpawnTimeModByPlayers(0)=1.1 // 1 player //1.1
LateWavesSpawnTimeModByPlayers(1)=1.45 // 2 players //1.1 //1.2--Aug15th 1.35
LateWavesSpawnTimeModByPlayers(2)=0.9 // 3 players //1.1
LateWavesSpawnTimeModByPlayers(3)=0.8 // 4 players //1.0
LateWavesSpawnTimeModByPlayers(4)=0.7 // 5 players //0.75
LateWavesSpawnTimeModByPlayers(5)=0.6 // 6 players //0.6
EarlyWaveSpawnRateModifier(0)=0.8 // Normal //1.0 //0.6
EarlyWaveSpawnRateModifier(1)=0.6 // Hard
EarlyWaveSpawnRateModifier(2)=0.5 // Suicidal //0.8
EarlyWaveSpawnRateModifier(3)=0.5 // Hell on Earth //0.7 //!!!!!0.5!!!!!! change back
//LateWaveSpawnRateModifier(0)=1.0 // Normal
//LateWaveSpawnRateModifier(1)=0.8 // Hard
//LateWaveSpawnRateModifier(2)=0.8 // Suicidal
//LateWaveSpawnRateModifier(3)=0.7 // Hell on Earth
RecycleSpecialSquad(0)=false // Normal
RecycleSpecialSquad(1)=false // Hard
RecycleSpecialSquad(2)=true // Suicidal
RecycleSpecialSquad(3)=true // Hell on Earth
MaxSpecialSquadRecycles=-1
MaxBossMinionScaleByPlayers(0)=1.0 // 1 player
MaxBossMinionScaleByPlayers(1)=1.5 // 2 players
MaxBossMinionScaleByPlayers(2)=1.5 // 3 players
MaxBossMinionScaleByPlayers(3)=1.875 // 4 players
MaxBossMinionScaleByPlayers(4)=1.875 // 5 players
MaxBossMinionScaleByPlayers(5)=2.0 // 6 players
bForceRequiredSquad=false
2022-10-29 23:52:58 +00:00
VIP_CurrentSpawnCounter = 0
VIP_MaxSpawnCounter = 5
2020-12-13 15:01:13 +00:00
}