//============================================================================= // 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 Waves; }; /** Wave settings per difficulty */ var array DifficultyWaveSettings; /** The current settings for this difficulty */ var DifficultyWaveInfo WaveSettings; /** The spawn group setup of a wave */ var array 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 MaxMonsters; }; var array PerDifficultyMaxMonsters; /** Used for arrays to modify spawn rate */ struct native SpawnRateModifier { var() array 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 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 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 > LeftoverSpawnSquad; /** The boss minion spawn squad we're currently spawning */ var array 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 RecentSpawnSelectedHumanControllerList; /* Used to force a specific boss */ var const int ForcedBossNum; var bool bTemporarilyEndless; /************************************************************************************ * 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); 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; if (OutbreakEvent.ActiveEvent.bBossRushMode) { NextWaveIndex = MyKFGRI.WaveMax - 1; } 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; if (MyKFGRI.IsBossWave() || OutbreakEvent.ActiveEvent.bBossRushMode) { 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 > 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 > 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 > AISpawnList) { local KFAISpawnSquad Squad; local EAIType AIType; local int i, j, RandNum; local ESquadType LargestMonsterSquadType; local array > TempSpawnList; local class ForcedPawnClass; local int RandBossIndex; 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 if (OutbreakEvent.ActiveEvent.bBossRushMode) { RandBossIndex = Rand(BossRushEnemies.length); TempSpawnList.AddItem( default.AIBossClassList[BossRushEnemies[RandBossIndex]]); BossRushEnemies.Remove(RandBossIndex, 1); } else { TempSpawnList.AddItem(GetBossAISpawnType()); } 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 > GetNextSpawnList() { local array< class > 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 > 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 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 > 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 > 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 > AIToSpawn, optional Controller OverrideController, optional Controller OtherController, optional bool bTeleporting, optional float MinDistSquared ) { local int VolumeIndex, ControllerIndex; local Controller RateController; 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); 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 }