diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..27ed978 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tools"] + path = tools + url = https://github.com/GenZmeY/KF2-BuildTools diff --git a/ZedSpawner/Classes/Spawn.uc b/ZedSpawner/Classes/Spawn.uc new file mode 100644 index 0000000..e59fafc --- /dev/null +++ b/ZedSpawner/Classes/Spawn.uc @@ -0,0 +1,58 @@ +class Spawn extends Object + dependson(ZedSpawner) + config(ZedSpawner); + +var config bool bCyclicalSpawn; +var config bool bShadowSpawn; +var config float ZedMultiplier; +var config float PlayerMultiplier; +var config float CycleMultiplier; +var config int AliveSpawnLimit; + +public static function InitConfig() +{ + default.bCyclicalSpawn = true; + default.bShadowSpawn = true; + default.ZedMultiplier = 1.0; + default.PlayerMultiplier = 0.25; + default.CycleMultiplier = 0.25; + default.AliveSpawnLimit = 0; + + StaticSaveConfig(); +} + +public static function bool Load(E_LogLevel LogLevel) +{ + local bool Errors; + + if (default.ZedMultiplier <= 0.f) + { + `ZS_Error("ZedMultiplier" @ "(" $ default.ZedMultiplier $ ")" @ "must be greater than 0.0", LogLevel); + Errors = true; + } + + if (default.PlayerMultiplier < 0.f) + { + `ZS_Error("PlayerMultiplier" @ "(" $ default.PlayerMultiplier $ ")" @ "must be greater than or equal 0.0", LogLevel); + Errors = true; + } + + if (default.CycleMultiplier < 0.f) + { + `ZS_Error("CycleMultiplier" @ "(" $ default.CycleMultiplier $ ")" @ "must be greater than or equal 0.0", LogLevel); + Errors = true; + } + + if (default.AliveSpawnLimit < 0) + { + `ZS_Error("AliveSpawnLimit" @ "(" $ default.AliveSpawnLimit $ ")" @ "must be greater than or equal 0", LogLevel); + Errors = true; + } + + return !Errors; +} + +defaultproperties +{ + +} diff --git a/ZedSpawner/Classes/SpawnList.uc b/ZedSpawner/Classes/SpawnList.uc new file mode 100644 index 0000000..b8c7f05 --- /dev/null +++ b/ZedSpawner/Classes/SpawnList.uc @@ -0,0 +1,125 @@ +class SpawnList extends Object + dependson(ZedSpawner) + config(ZedSpawner); + +struct S_SpawnEntryCfg +{ + var int Wave; + var String ZedClass; + var int RelativeDelay; + var int Delay; + var int Probability; + var int SpawnAtOnce; + var int MaxSpawns; + var bool bSpawnAtPlayerStart; +}; + +var config Array Spawn; + +public static function InitConfig() +{ + local S_SpawnEntryCfg SpawnEntry; + + default.Spawn.Length = 0; + + SpawnEntry.Wave = 1; + SpawnEntry.ZedClass = "SomePackage.SomeZedClass1"; + SpawnEntry.SpawnAtOnce = 1; + SpawnEntry.MaxSpawns = 1; + SpawnEntry.RelativeDelay = 0; + SpawnEntry.Delay = 60; + SpawnEntry.Probability = 100; + SpawnEntry.bSpawnAtPlayerStart = false; + default.Spawn.AddItem(SpawnEntry); + + SpawnEntry.Wave = 2; + SpawnEntry.ZedClass = "SomePackage.SomeZedClass2"; + SpawnEntry.SpawnAtOnce = 2; + SpawnEntry.MaxSpawns = 4; + SpawnEntry.RelativeDelay = 0; + SpawnEntry.Delay = 30; + SpawnEntry.Probability = 50; + SpawnEntry.bSpawnAtPlayerStart = false; + default.Spawn.AddItem(SpawnEntry); + + StaticSaveConfig(); +} + +public static function Array Load(E_LogLevel LogLevel) +{ + local Array SpawnList; + local S_SpawnEntryCfg SpawnEntryCfg; + local S_SpawnEntry SpawnEntry; + local int Line; + local bool Errors; + + `ZS_Info("Load spawn list:", LogLevel); + foreach default.Spawn(SpawnEntryCfg, Line) + { + Errors = false; + + SpawnEntry.ZedClass = class(DynamicLoadObject(SpawnEntryCfg.ZedClass, class'Class')); + if (SpawnEntry.ZedClass == None) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Can't load zed class:" @ SpawnEntryCfg.ZedClass, LogLevel); + Errors = true; + } + + SpawnEntry.Wave = SpawnEntryCfg.Wave; + if (SpawnEntryCfg.Wave <= 0 || SpawnEntryCfg.Wave > 255) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Wave" @ "(" $ SpawnEntryCfg.ZedClass $ ")" @ "must be greater than 0 and less than 256", LogLevel); + Errors = true; + } + + SpawnEntry.SpawnAtOnce = SpawnEntryCfg.SpawnAtOnce; + if (SpawnEntry.SpawnAtOnce <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "SpawnAtOnce" @ "(" $ SpawnEntryCfg.SpawnAtOnce $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.Probability = SpawnEntryCfg.Probability / 100.f; + if (SpawnEntryCfg.Probability <= 0 || SpawnEntryCfg.Probability > 100) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Probability" @ "(" $ SpawnEntryCfg.Probability $ ")" @ "must be greater than 0 and less than or equal 100", LogLevel); + Errors = true; + } + + SpawnEntry.RelativeDelayDefault = SpawnEntryCfg.RelativeDelay / 100.f; + if (SpawnEntryCfg.RelativeDelay < 0 || SpawnEntryCfg.RelativeDelay > 100) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "RelativeDelay" @ "(" $ SpawnEntryCfg.RelativeDelay $ ")" @ "must be greater than or equal 0 and less than or equal 100", LogLevel); + Errors = true; + } + + SpawnEntry.DelayDefault = SpawnEntryCfg.Delay; + if (SpawnEntryCfg.Delay <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Delay" @ "(" $ SpawnEntryCfg.Delay $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.MaxSpawns = SpawnEntryCfg.MaxSpawns; + if (SpawnEntryCfg.Delay <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "MaxSpawns" @ "(" $ SpawnEntryCfg.MaxSpawns $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.SpawnAtPlayerStart = SpawnEntryCfg.bSpawnAtPlayerStart; + + if (!Errors) + { + SpawnList.AddItem(SpawnEntry); + `ZS_Info("[" $ Line + 1 $ "]" @ "Loaded successfully:" @ SpawnEntryCfg.Wave @ SpawnEntryCfg.ZedClass, LogLevel); + } + } + + return SpawnList; +} + +defaultproperties +{ + +} diff --git a/ZedSpawner/Classes/SpawnListBossWaves.uc b/ZedSpawner/Classes/SpawnListBossWaves.uc new file mode 100644 index 0000000..1ead012 --- /dev/null +++ b/ZedSpawner/Classes/SpawnListBossWaves.uc @@ -0,0 +1,111 @@ +class SpawnListBossWaves extends Object + dependson(ZedSpawner) + config(ZedSpawner); + +struct S_SpawnEntryCfg +{ + var String BossClass; + var String ZedClass; + var int Delay; + var int Probability; + var int SpawnAtOnce; + var int MaxSpawns; + var bool bSpawnAtPlayerStart; +}; + +var config bool bStopRegularSpawn; +var config Array Spawn; + +public static function InitConfig() +{ + local S_SpawnEntryCfg SpawnEntry; + + default.bStopRegularSpawn = true; + + default.Spawn.Length = 0; + + SpawnEntry.BossClass = "KFGameContent.KFPawn_ZedFleshpoundKing"; + SpawnEntry.ZedClass = "SomePackage.SomeFleshpoundClass"; + SpawnEntry.SpawnAtOnce = 1; + SpawnEntry.MaxSpawns = 1; + SpawnEntry.Delay = 60; + SpawnEntry.Probability = 1; + SpawnEntry.bSpawnAtPlayerStart = false; + default.Spawn.AddItem(SpawnEntry); + + StaticSaveConfig(); +} + +public static function Array Load(E_LogLevel LogLevel) +{ + local Array SpawnList; + local S_SpawnEntryCfg SpawnEntryCfg; + local S_SpawnEntry SpawnEntry; + local int Line; + local bool Errors; + + `ZS_Info("Load boss waves spawn list:", LogLevel); + foreach default.Spawn(SpawnEntryCfg, Line) + { + Errors = false; + + SpawnEntry.BossClass = class(DynamicLoadObject(SpawnEntryCfg.BossClass, class'Class')); + if (SpawnEntry.BossClass == None) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Can't load boss class:" @ SpawnEntryCfg.BossClass, LogLevel); + Errors = true; + } + + SpawnEntry.ZedClass = class(DynamicLoadObject(SpawnEntryCfg.ZedClass, class'Class')); + if (SpawnEntry.ZedClass == None) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Can't load zed class:" @ SpawnEntryCfg.ZedClass, LogLevel); + Errors = true; + } + + SpawnEntry.SpawnAtOnce = SpawnEntryCfg.SpawnAtOnce; + if (SpawnEntry.SpawnAtOnce <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "SpawnAtOnce" @ "(" $ SpawnEntryCfg.SpawnAtOnce $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.Probability = SpawnEntryCfg.Probability / 100.f; + if (SpawnEntryCfg.Probability <= 0 || SpawnEntryCfg.Probability > 100) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Probability" @ "(" $ SpawnEntryCfg.Probability $ ")" @ "must be greater than 0 and less than or equal 100", LogLevel); + Errors = true; + } + + SpawnEntry.RelativeDelayDefault = 0.f; + + SpawnEntry.DelayDefault = SpawnEntryCfg.Delay; + if (SpawnEntryCfg.Delay <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Delay" @ "(" $ SpawnEntryCfg.Delay $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.MaxSpawns = SpawnEntryCfg.MaxSpawns; + if (SpawnEntryCfg.Delay <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "MaxSpawns" @ "(" $ SpawnEntryCfg.MaxSpawns $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.SpawnAtPlayerStart = SpawnEntryCfg.bSpawnAtPlayerStart; + + if (!Errors) + { + SpawnList.AddItem(SpawnEntry); + `ZS_Info("[" $ Line + 1 $ "]" @ "Loaded successfully:" @ SpawnEntryCfg.BossClass @ SpawnEntryCfg.ZedClass, LogLevel); + } + } + + return SpawnList; +} + +defaultproperties +{ + +} diff --git a/ZedSpawner/Classes/SpawnListSpecialWaves.uc b/ZedSpawner/Classes/SpawnListSpecialWaves.uc new file mode 100644 index 0000000..f458da2 --- /dev/null +++ b/ZedSpawner/Classes/SpawnListSpecialWaves.uc @@ -0,0 +1,124 @@ +class SpawnListSpecialWaves extends Object + dependson(ZedSpawner) + config(ZedSpawner); + +struct S_SpawnEntryCfg +{ + var EAIType Wave; + var String ZedClass; + var int RelativeDelay; + var int Delay; + var int Probability; + var int SpawnAtOnce; + var int MaxSpawns; + var bool bSpawnAtPlayerStart; +}; + +var config bool bStopRegularSpawn; +var config Array Spawn; + +public static function InitConfig() +{ + local S_SpawnEntryCfg SpawnEntry; + + default.bStopRegularSpawn = true; + + default.Spawn.Length = 0; + + SpawnEntry.Wave = AT_Husk; + SpawnEntry.ZedClass = "SomePackage.SomeHuskClass"; + SpawnEntry.SpawnAtOnce = 1; + SpawnEntry.MaxSpawns = 1; + SpawnEntry.RelativeDelay = 0; + SpawnEntry.Delay = 60; + SpawnEntry.Probability = 1; + SpawnEntry.bSpawnAtPlayerStart = false; + default.Spawn.AddItem(SpawnEntry); + + StaticSaveConfig(); +} + +public static function Array Load(KFGameInfo_Endless KFGIE, E_LogLevel LogLevel) +{ + local Array SpawnList; + local S_SpawnEntryCfg SpawnEntryCfg; + local S_SpawnEntry SpawnEntry; + local int Line; + local bool Errors; + + if (KFGIE == None) + { + `ZS_Info("Not Endless mode, skip loading special waves", LogLevel); + return SpawnList; + } + + `ZS_Info("Load special waves spawn list:", LogLevel); + foreach default.Spawn(SpawnEntryCfg, Line) + { + Errors = false; + + SpawnEntry.Wave = SpawnEntryCfg.Wave; + if (KFGIE.SpecialWaveTypes.Find(EAIType(SpawnEntryCfg.Wave)) == INDEX_NONE) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Unknown special wave:" @ SpawnEntryCfg.Wave, LogLevel); + Errors = true; + } + + SpawnEntry.ZedClass = class(DynamicLoadObject(SpawnEntryCfg.ZedClass, class'Class')); + if (SpawnEntry.ZedClass == None) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Can't load zed class:" @ SpawnEntryCfg.ZedClass, LogLevel); + Errors = true; + } + + SpawnEntry.SpawnAtOnce = SpawnEntryCfg.SpawnAtOnce; + if (SpawnEntry.SpawnAtOnce <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "SpawnAtOnce" @ "(" $ SpawnEntryCfg.SpawnAtOnce $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.Probability = SpawnEntryCfg.Probability / 100.f; + if (SpawnEntryCfg.Probability <= 0 || SpawnEntryCfg.Probability > 100) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Probability" @ "(" $ SpawnEntryCfg.Probability $ ")" @ "must be greater than 0 and less than or equal 100", LogLevel); + Errors = true; + } + + SpawnEntry.RelativeDelayDefault = SpawnEntryCfg.RelativeDelay / 100.f; + if (SpawnEntryCfg.RelativeDelay < 0 || SpawnEntryCfg.RelativeDelay > 100) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "RelativeDelay" @ "(" $ SpawnEntryCfg.RelativeDelay $ ")" @ "must be greater than or equal 0 and less than or equal 100", LogLevel); + Errors = true; + } + + SpawnEntry.DelayDefault = SpawnEntryCfg.Delay; + if (SpawnEntryCfg.Delay <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Delay" @ "(" $ SpawnEntryCfg.Delay $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.MaxSpawns = SpawnEntryCfg.MaxSpawns; + if (SpawnEntryCfg.Delay <= 0) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "MaxSpawns" @ "(" $ SpawnEntryCfg.MaxSpawns $ ")" @ "must be greater than 0", LogLevel); + Errors = true; + } + + SpawnEntry.SpawnAtPlayerStart = SpawnEntryCfg.bSpawnAtPlayerStart; + + if (!Errors) + { + SpawnList.AddItem(SpawnEntry); + `ZS_Info("[" $ Line + 1 $ "]" @ "Loaded successfully:" @ SpawnEntryCfg.Wave @ SpawnEntryCfg.ZedClass, LogLevel); + } + } + + return SpawnList; +} + +defaultproperties +{ + +} diff --git a/ZedSpawner/Classes/ZedSpawner.uc b/ZedSpawner/Classes/ZedSpawner.uc new file mode 100644 index 0000000..b89b7bb --- /dev/null +++ b/ZedSpawner/Classes/ZedSpawner.uc @@ -0,0 +1,552 @@ +class ZedSpawner extends Info + config(ZedSpawner); + +var const int dt; + +var const class CfgSpawn; +var const class CfgSpawnList; +var const class CfgSpawnListBW; +var const class CfgSpawnListSW; + +enum E_LogLevel +{ + LL_WrongLevel, + LL_Fatal, + LL_Error, + LL_Warning, + LL_Info, + LL_Debug, + LL_Trace, + LL_All +}; + +struct S_SpawnEntry +{ + var class BossClass; + var class ZedClass; + var int Wave; + var int SpawnAtOnce; + var float Probability; + var float RelativeDelayDefault; + var float RelativeDelay; + var int DelayDefault; + var int Delay; + var int SpawnsDone; + var int MaxSpawns; + var bool SpawnAtPlayerStart; +}; + +var config bool bConfigInitialized; +var config E_LogLevel LogLevel; + +var private Array SpawnList; +var private Array SpawnListBW; +var private Array SpawnListSW; + +var private KFGameInfo_Survival KFGIS; +var private KFGameInfo_Endless KFGIE; + +var private int CurrentWave; +var private int CurrentCycle; +var private int CycleWaveShift; +var private int CycleWaveSize; + +var private int WaveTotalAI; +var private class CurrentBossClass; + +var private String SpawnTimerLastMessage; + +var private Array > BossClassCache; + +event PostBeginPlay() +{ + `ZS_Trace(`Location, LogLevel); + + Super.PostBeginPlay(); + + if (WorldInfo.NetMode == NM_Client) + { + Destroy(); + return; + } + + Init(); +} + +private function Init() +{ + local S_SpawnEntry SpawnEntry; + + `ZS_Trace(`Location, LogLevel); + + if (!bConfigInitialized) + { + bConfigInitialized = true; + LogLevel = LL_Info; + SaveConfig(); + CfgSpawn.static.InitConfig(); + CfgSpawnList.static.InitConfig(); + CfgSpawnListBW.static.InitConfig(); + CfgSpawnListSW.static.InitConfig(); + `ZS_Info("Config initialized.", LogLevel); + } + + if (LogLevel == LL_WrongLevel) + { + LogLevel = LL_Info; + `ZS_Warn("Wrong 'LogLevel', return to default value", LogLevel); + SaveConfig(); + } + + `ZS_Log("LogLevel:" @ LogLevel); + + if (!CfgSpawn.static.Load(LogLevel)) + { + `ZS_Fatal("Wrong settings, Destroy...", LogLevel); + Destroy(); + return; + } + + CurrentWave = INDEX_NONE; + KFGIS = KFGameInfo_Survival(WorldInfo.Game); + if (KFGIS == None) + { + `ZS_Fatal("Incompatible gamemode:" @ WorldInfo.Game $ ". Destroy...", LogLevel); + Destroy(); + return; + } + + KFGIE = KFGameInfo_Endless(KFGIS); + + SpawnList = CfgSpawnList.static.Load(LogLevel); + SpawnListBW = CfgSpawnListBW.static.Load(LogLevel); + SpawnListSW = CfgSpawnListSW.static.Load(KFGIE, LogLevel); + + foreach SpawnListBW(SpawnEntry) + BossClassCache.AddItem(SpawnEntry.BossClass); + + CurrentCycle = 1; + CycleWaveSize = 0; + CycleWaveShift = MaxInt; + foreach SpawnList(SpawnEntry) + { + CycleWaveShift = Min(CycleWaveShift, SpawnEntry.Wave); + CycleWaveSize = Max(CycleWaveSize, SpawnEntry.Wave); + } + CycleWaveSize = CycleWaveSize - CycleWaveShift + 1; + + SetTimer(float(dt), true, nameof(SpawnTimer)); +} + +private function SpawnTimer() +{ + local int WaveTotalAIDef; + local int SpecialWave; + + `ZS_Trace(`Location, LogLevel); + + if (CurrentWave < KFGIS.WaveNum) + { + CurrentWave = KFGIS.WaveNum; + + if (!KFGIS.MyKFGRI.IsBossWave()) + { + WaveTotalAIDef = KFGIS.SpawnManager.WaveTotalAI; + KFGIS.SpawnManager.WaveTotalAI *= CfgSpawn.default.ZedMultiplier; + WaveTotalAI = KFGIS.SpawnManager.WaveTotalAI; + KFGIS.MyKFGRI.WaveTotalAICount = KFGIS.SpawnManager.WaveTotalAI; + KFGIS.MyKFGRI.AIRemaining = KFGIS.SpawnManager.WaveTotalAI; + + if (WaveTotalAIDef != KFGIS.SpawnManager.WaveTotalAI) + { + `ZS_Info("increase WaveTotalAI from" @ WaveTotalAIDef @ "to" @ WaveTotalAI @ "due to ZedMultiplier" @ "(" $ CfgSpawn.default.ZedMultiplier $ ")", LogLevel); + } + } + + if (CfgSpawn.default.bCyclicalSpawn && KFGIS.WaveNum > 1 && KFGIS.WaveNum == CycleWaveShift + CycleWaveSize * CurrentCycle) + { + CurrentCycle++; + `ZS_Info("Next spawn cycle started:" @ CurrentCycle, LogLevel); + } + + ResetSpawnList(SpawnList); + ResetSpawnList(SpawnListSW); + ResetSpawnList(SpawnListBW); + + CurrentBossClass = None; + } + + if (!KFGIS.IsWaveActive()) + { + SpawnTimerLogger(true, "wave is not active"); + return; + } + + if (CfgSpawn.default.AliveSpawnLimit > 0 && KFGIS.AIAliveCount >= CfgSpawn.default.AliveSpawnLimit) + { + SpawnTimerLogger(true, "alive spawn limit reached"); + return; + } + + if (!KFGIS.MyKFGRI.IsBossWave() && CfgSpawn.default.bShadowSpawn && KFGIS.MyKFGRI.AIRemaining <= KFGIS.AIAliveCount) + { + SpawnTimerLogger(true, "shadow spawn is active and no free spawn slots"); + return; + } + + SpawnTimerLogger(false); + + SpecialWave = INDEX_NONE; + if (KFGIE != None) + { + SpecialWave = KFGameReplicationInfo_Endless(KFGIE.GameReplicationInfo).CurrentSpecialMode; + } + + if ((SpecialWave == INDEX_NONE && !KFGIS.MyKFGRI.IsBossWave()) + || (SpecialWave != INDEX_NONE && !CfgSpawnListSW.default.bStopRegularSpawn) + || (KFGIS.MyKFGRI.IsBossWave() && !CfgSpawnListBW.default.bStopRegularSpawn)) + SpawnRegularWaveZeds(); + + if (SpecialWave != INDEX_NONE) + SpawnSpecialWaveZeds(SpecialWave); + + if (KFGIS.MyKFGRI.IsBossWave()) + SpawnBossWaveZeds(); +} + +private function ResetSpawnList(out Array List) +{ + local S_SpawnEntry SpawnEntry; + local int i; + + `ZS_Trace(`Location, LogLevel); + + foreach List(SpawnEntry, i) + { + List[i].SpawnsDone = 0; + + if (KFGIS.MyKFGRI.IsBossWave()) + { + List[i].RelativeDelay = 0.f; + List[i].Delay = SpawnEntry.DelayDefault; + } + else + { + List[i].RelativeDelay = SpawnEntry.RelativeDelayDefault; + if (SpawnEntry.RelativeDelay == 0.f) + List[i].Delay = SpawnEntry.DelayDefault; + else + List[i].Delay = 0; + } + } +} + +private function SpawnTimerLogger(bool Stop, optional String Reason) +{ + local String Message; + + `ZS_Trace(`Location, LogLevel); + + if (Stop) + Message = "Stop spawn"; + else + Message = "Start spawn"; + + if (Reason != "") + Message @= "(" $ Reason $ ")"; + + if (Message != SpawnTimerLastMessage) + { + `ZS_Info(Message, LogLevel); + SpawnTimerLastMessage = Message; + } +} + +private function SpawnRegularWaveZeds() +{ + local S_SpawnEntry SpawnEntry; + local int Spawned, SpawnsLeft, i; + local String Message; + + `ZS_Trace(`Location, LogLevel); + + foreach SpawnList(SpawnEntry, i) + { + if (SpawnEntry.Wave == KFGIS.WaveNum - CycleWaveSize * (CurrentCycle - 1)) + { + SpawnList[i].Delay -= dt; + if (TimeToSpawn(SpawnEntry)) + { + SpawnList[i].Delay = SpawnEntry.DelayDefault; + if (FRand() <= SpawnEntry.Probability) + { + Spawned = SpawnZed(SpawnEntry.ZedClass, SpawnEntry.SpawnAtOnce, SpawnEntry.SpawnAtPlayerStart); + Message = "Spawned:" @ SpawnEntry.ZedClass @ "x" $ Spawned; + } + else + { + Message = "Skip spawn" @ SpawnEntry.ZedClass @ "due to probability:" @ SpawnEntry.Probability * 100 $ "%"; + } + + SpawnList[i].SpawnsDone++; + SpawnsLeft = SpawnEntry.MaxSpawns - SpawnList[i].SpawnsDone; + if (SpawnsLeft > 0) + { + Message @= "(Next spawn after" @ SpawnEntry.DelayDefault $ "sec," @ "spawns left:" @ SpawnsLeft $ ")"; + } + `ZS_Info(Message, LogLevel); + } + } + } +} + +private function SpawnSpecialWaveZeds(int SpecialWave) +{ + local S_SpawnEntry SpawnEntry; + local int Spawned, SpawnsLeft, i; + local String Message; + + `ZS_Trace(`Location, LogLevel); + + foreach SpawnListSW(SpawnEntry, i) + { + if (SpawnEntry.Wave == SpecialWave) + { + SpawnListSW[i].Delay -= dt; + + if (TimeToSpawn(SpawnEntry)) + { + SpawnListSW[i].Delay = SpawnEntry.DelayDefault; + if (FRand() <= SpawnEntry.Probability) + { + Spawned = SpawnZed(SpawnEntry.ZedClass, SpawnEntry.SpawnAtOnce, SpawnEntry.SpawnAtPlayerStart); + Message = "Spawned:" @ SpawnEntry.ZedClass @ "x" $ Spawned; + } + else + { + Message = "Skip spawn" @ SpawnEntry.ZedClass @ "due to probability:" @ SpawnEntry.Probability * 100 $ "%"; + } + + SpawnListSW[i].SpawnsDone++; + SpawnsLeft = SpawnEntry.MaxSpawns - SpawnListSW[i].SpawnsDone; + if (SpawnsLeft > 0) + { + Message @= "(Next spawn after" @ SpawnEntry.DelayDefault $ "sec," @ "spawns left:" @ SpawnsLeft $ ")"; + } + `ZS_Info(Message, LogLevel); + } + } + } +} + +private function SpawnBossWaveZeds() +{ + local S_SpawnEntry SpawnEntry; + local KFPawn_Monster KFPM; + local int Spawned, SpawnsLeft, i; + local String Message; + + `ZS_Trace(`Location, LogLevel); + + if (CurrentBossClass == None) + { + foreach WorldInfo.AllPawns(class'KFPawn_Monster', KFPM) + { + i = BossClassCache.Find(KFPM.class); + if (i != INDEX_NONE) + { + CurrentBossClass = BossClassCache[i]; + break; + } + } + } + + if (CurrentBossClass == None) + { + return; + } + + foreach SpawnListBW(SpawnEntry, i) + { + if (SpawnEntry.BossClass == CurrentBossClass) + { + SpawnListBW[i].Delay -= dt; + if (TimeToSpawn(SpawnEntry)) + { + SpawnListBW[i].Delay = SpawnEntry.DelayDefault; + if (FRand() <= SpawnEntry.Probability) + { + Spawned = SpawnZed(SpawnEntry.ZedClass, SpawnEntry.SpawnAtOnce, SpawnEntry.SpawnAtPlayerStart); + Message = "Spawned:" @ SpawnEntry.ZedClass @ "x" $ Spawned; + } + else + { + Message = "Skip spawn" @ SpawnEntry.ZedClass @ "due to probability:" @ SpawnEntry.Probability * 100 $ "%"; + } + + SpawnListBW[i].SpawnsDone++; + SpawnsLeft = SpawnEntry.MaxSpawns - SpawnListBW[i].SpawnsDone; + if (SpawnsLeft > 0) + { + Message @= "(Next spawn after" @ SpawnEntry.DelayDefault $ "sec," @ "spawns left:" @ SpawnsLeft $ ")"; + } + `ZS_Info(Message, LogLevel); + } + } + } +} + +private function bool TimeToSpawn(S_SpawnEntry SpawnEntry) +{ + local bool DelayReady, DelayRelativeReady; + + `ZS_Trace(`Location, LogLevel); + + DelayReady = SpawnEntry.Delay <= 0 && SpawnEntry.MaxSpawns >= 0 && SpawnEntry.SpawnsDone < SpawnEntry.MaxSpawns; + + if (SpawnEntry.RelativeDelay == 0.f) + { + DelayRelativeReady = true; + } + else + { + DelayRelativeReady = (SpawnEntry.RelativeDelay <= 1.0f - (float(KFGIS.MyKFGRI.AIRemaining) / float(WaveTotalAI))); + } + + return DelayReady && DelayRelativeReady; +} + +private function int PlayerCount() +{ + local PlayerController PC; + local int HumanPlayers; + local KFOnlineGameSettings KFGameSettings; + + `ZS_Trace(`Location, LogLevel); + + if (KFGIS.PlayfabInter != None && KFGIS.PlayfabInter.GetGameSettings() != None) + { + KFGameSettings = KFOnlineGameSettings(KFGIS.PlayfabInter.GetGameSettings()); + HumanPlayers = KFGameSettings.NumPublicConnections - KFGameSettings.NumOpenPublicConnections; + } + else + { + HumanPlayers = 0; + foreach WorldInfo.AllControllers(class'PlayerController', PC) + if (PC.bIsPlayer + && PC.PlayerReplicationInfo != None + && !PC.PlayerReplicationInfo.bOnlySpectator + && !PC.PlayerReplicationInfo.bBot) + HumanPlayers++; + } + + return HumanPlayers; +} + +private function Vector PlayerStartLocation() +{ + local PlayerController PC; + + `ZS_Trace(`Location, LogLevel); + + foreach WorldInfo.AllControllers(class'PlayerController', PC) + return KFGIS.FindPlayerStart(PC, 0).Location; + + return KFGIS.FindPlayerStart(None, 0).Location; +} + +private function int SpawnZed(class ZedClass, int SpawnAtOnce, bool SpawnAtPlayerStart) +{ + local Array > CustomSquad; + local Vector SpawnLocation; + local KFPawn_Monster KFPM; + local Controller C; + local int ModdedSpawnCount; + local int FreeSpawnSlots; + local int SpawnFailed; + local int i; + + `ZS_Trace(`Location, LogLevel); + + ModdedSpawnCount = Round(float(SpawnAtOnce) * (1.0f + float(CurrentCycle - 1) * CfgSpawn.default.CycleMultiplier + float(PlayerCount() - 1) * CfgSpawn.default.PlayerMultiplier)); + + if (CfgSpawn.default.bShadowSpawn && !KFGIS.MyKFGRI.IsBossWave()) + { + FreeSpawnSlots = KFGIS.MyKFGRI.AIRemaining - KFGIS.AIAliveCount; + if (ModdedSpawnCount > FreeSpawnSlots) + { + `ZS_Info("Not enough free slots to spawn, will spawn" @ FreeSpawnSlots @ "instead of" @ ModdedSpawnCount, LogLevel); + ModdedSpawnCount = FreeSpawnSlots; + } + } + + for (i = 0; i < ModdedSpawnCount; i++) + CustomSquad.AddItem(ZedClass); + + if (SpawnAtPlayerStart) + { + SpawnLocation = PlayerStartLocation(); + SpawnLocation.Y += 64; + SpawnLocation.Z += 64; + } + else + { + SpawnLocation = KFGIS.SpawnManager.GetBestSpawnVolume(CustomSquad).Location; + SpawnLocation.Z += 10; + } + + SpawnFailed = 0; + for (i = 0; i < ModdedSpawnCount; i++) + { + KFPM = Spawn(ZedClass,,, SpawnLocation, rot(0,0,1),, true); + if (KFPM == None) + { + `ZS_Error("Can't spawn" @ ZedClass, LogLevel); + SpawnFailed++; + continue; + } + + C = KFPM.Spawn(KFPM.ControllerClass); + if (C == None) + { + `ZS_Error("Can't spawn controller for" @ ZedClass $ ". Destroy this KFPawn...", LogLevel); + KFPM.Destroy(); + SpawnFailed++; + continue; + } + C.Possess(KFPM, false); + } + + if (CfgSpawn.default.bShadowSpawn && !KFGIS.MyKFGRI.IsBossWave()) + { + KFGIS.MyKFGRI.AIRemaining -= (ModdedSpawnCount - SpawnFailed); + } + + KFGIS.RefreshMonsterAliveCount(); + + return ModdedSpawnCount - SpawnFailed; +} + +private function PrintSpawnEntry(S_SpawnEntry SE) +{ + `ZS_Debug("BossClass:" @ SE.BossClass, LogLevel); + `ZS_Debug("ZedClass:" @ SE.ZedClass, LogLevel); + `ZS_Debug("Wave:" @ SE.Wave, LogLevel); + `ZS_Debug("SpawnAtOnce:" @ SE.SpawnAtOnce, LogLevel); + `ZS_Debug("Probability:" @ SE.Probability, LogLevel); + `ZS_Debug("RelativeDelay:" @ SE.RelativeDelay @ "(" $ SE.RelativeDelayDefault $ ")", LogLevel); + `ZS_Debug("Delay:" @ SE.Delay @ "(" $ SE.DelayDefault $ ")", LogLevel); + `ZS_Debug("SpawnsDone:" @ SE.SpawnsDone @ "of" @ SE.MaxSpawns, LogLevel); + `ZS_Debug("SpawnAtPlayerStart:" @ SE.SpawnAtPlayerStart, LogLevel); + `ZS_Debug("---------------------", LogLevel); +} + +DefaultProperties +{ + dt = 1 + + CfgSpawn = class'Spawn' + CfgSpawnList = class'SpawnList' + CfgSpawnListBW = class'SpawnListBossWaves' + CfgSpawnListSW = class'SpawnListSpecialWaves' +} \ No newline at end of file diff --git a/ZedVarient/Classes/ZedVarient.UPKG b/ZedSpawner/Classes/ZedSpawner.upkg similarity index 100% rename from ZedVarient/Classes/ZedVarient.UPKG rename to ZedSpawner/Classes/ZedSpawner.upkg diff --git a/ZedSpawner/Classes/ZedSpawnerMut.uc b/ZedSpawner/Classes/ZedSpawnerMut.uc new file mode 100644 index 0000000..7aa98f3 --- /dev/null +++ b/ZedSpawner/Classes/ZedSpawnerMut.uc @@ -0,0 +1,44 @@ +class ZedSpawnerMut extends KFMutator + dependson(ZedSpawner); + +var private ZedSpawner ZS; + +event PreBeginPlay() +{ + Super.PreBeginPlay(); + + if (WorldInfo.NetMode == NM_Client) return; + + foreach WorldInfo.DynamicActors(class'ZedSpawner', ZS) + { + `ZS_Log("Found 'ZedSpawner'"); + break; + } + + if (ZS == None) + { + `ZS_Log("Spawn 'ZedSpawner'"); + ZS = WorldInfo.Spawn(class'ZedSpawner'); + } + + if (ZS == None) + { + `ZS_Log("Can't Spawn 'ZedSpawner', Destroy..."); + Destroy(); + } +} + +public function AddMutator(Mutator Mut) +{ + if (Mut == Self) return; + + if (Mut.Class == Class) + Mut.Destroy(); + else + Super.AddMutator(Mut); +} + +DefaultProperties +{ + +} \ No newline at end of file diff --git a/ZedSpawner/Globals.uci b/ZedSpawner/Globals.uci new file mode 100644 index 0000000..ed1e090 --- /dev/null +++ b/ZedSpawner/Globals.uci @@ -0,0 +1,10 @@ +`define ZS_Tag 'ZedSpawner' + +`define ZS_Log(msg, cond) `log(`msg `if(`cond), `cond`{endif}, `ZS_Tag) + +`define ZS_Fatal(msg, level) `log("FATAL:" @ `msg, (`level >= LL_Fatal), `ZS_Tag) +`define ZS_Error(msg, level) `log("ERROR:" @ `msg, (`level >= LL_Error), `ZS_Tag) +`define ZS_Warn(msg, level) `log("WARN:" @ `msg, (`level >= LL_Warning), `ZS_Tag) +`define ZS_Info(msg, level) `log("INFO:" @ `msg, (`level >= LL_Info), `ZS_Tag) +`define ZS_Debug(msg, level) `log("DEBUG:" @ `msg, (`level >= LL_Debug), `ZS_Tag) +`define ZS_Trace(msg, level) `log("TRACE:" @ `msg, (`level >= LL_Trace), `ZS_Tag) diff --git a/ZedVarient/Classes/ZedVarient.uc b/ZedVarient/Classes/ZedVarient.uc deleted file mode 100644 index ec6c019..0000000 --- a/ZedVarient/Classes/ZedVarient.uc +++ /dev/null @@ -1,310 +0,0 @@ -class ZedVarient extends KFMutator - config(ZedVarient); - -//Timer rate -var const float dt; - -//User out cfg -struct TZedCfg -{ - var int Wave; - var int SpawnAtOnce; - var int SpawnsDone; - var int MaxSpawns; - var int Spawnsleft; - var string Zed; - var float Probability; - var float Delay; -}; - -//Mut inner cfg -struct TZedCfgTmp -{ - var int Wave; - var int SpawnAtOnce; - var class Zed; - var float Probability; - var float DefDelay; - var float Delay; - var int SpawnsDone; - var int MaxSpawns; - var int SpawnsLeft; -}; - -var config float ZedMultiplier; -var config bool bConfigsInit; -var config array CustomZeds; -var config array Bosses; - -var array< class > LoadedBosses; -var array< TZedCfgTmp > LoadedCustomZeds; -var KFGameInfo_Survival KFGT; -var int cwave; -var array UZBosses; -var bool bFB; -var KFPawn_Monster OriginalBoss; -var int BossesLeft; -//Dunno how to properly calculate using original stuff -var int NeedMoreZeds; -var int MaxSpawns; -var int SpawnsLeft; -var int SpawnsDone; - -function PostBeginPlay() -{ - local int i; - local class C; - local TZedCfgTmp zct; - - for(i=0;i(DynamicLoadObject(Bosses[i],Class'Class')); - - if(C!=None) - LoadedBosses.AddItem(C); - else - LogInternal("Error while loading"@Bosses[i]); - } - - if(!bConfigsInit) - { - bConfigsInit = true; - ZedMultiplier = 1.0; - SaveConfig(); - } - - cwave=-1; - KFGT = KFGameInfo_Survival(WorldInfo.Game); - - //Init inner cfg - for(i=0;i(DynamicLoadObject(CustomZeds[i].Zed,Class'Class')); - - if(C!=None && CustomZeds[i].Wave>0 && CustomZeds[i].SpawnAtOnce>0 && CustomZeds[i].Probability>0 && CustomZeds[i].Delay>0 && CustomZeds[i].MaxSpawns>0) - { - LogInternal("LOADED"@CustomZeds[i].Zed); - zct.Wave = CustomZeds[i].Wave; - zct.SpawnAtOnce = CustomZeds[i].SpawnAtOnce; - zct.Probability = CustomZeds[i].Probability; - zct.DefDelay = CustomZeds[i].Delay; - zct.Delay = CustomZeds[i].Delay; - zct.MaxSpawns = CustomZeds[i].MaxSpawns; - zct.SpawnsLeft = CustomZeds[i].MaxSpawns - CustomZeds[i].SpawnsDone; - zct.Zed = C; - LoadedCustomZeds.AddItem(zct); - } - else - LogInternal("Error while loading"@CustomZeds[i].Zed); - } - - SetTimer(dt,true); -} - -function Timer() -{ - //setup total amount multiplier - if(cwave > CSquad; - local KFSpawnVolume KFSV; - - KFSV = KFGT.SpawnManager.GetBestSpawnVolume(CSquad); - - if( !KFGT.IsWaveActive() || NeedMoreZeds<=0 || KFGT.AIAliveCount>128 || KFSV.Location==PlayerController(Owner).StartSpot.Location ) - return; // Maxmonsters, ????, ?? ???? ?? //VSize(KFSV.Location-PlayerController(Owner).Pawn.Location)<650.f //KFSV.bNoCollisionFailForSpawn==true - - for(i=0;i0 && LoadedCustomZeds[i].MaxSpawns>=0 && (LoadedCustomZeds[i].SpawnsDone ZedClass ) -{ - local KFPawn_Monster M; - local Controller C; - - if( ZedClass==class'HL2Monsters.Combine_Strider' || ZedClass==class'HL2Monsters.Combine_Gunship' || ZedClass==class'HL2Monsters.Hunter_Chopper' ) - { - L = KFGameInfo(WorldInfo.Game).FindPlayerStart(PlayerController(Owner),0).Location; - L.Y += 64; - L.Z += 64; - } - else L.Z += 10; - - M = Spawn(ZedClass,,,L,rot(0,0,1),,true); - - if( M==None ) - return; - - C = M.Spawn(M.ControllerClass); - C.Possess(M,false); - KFGT.MyKFGRI.AIRemaining+=1; //added - KFGT.NumAISpawnsQueued++; - KFGT.AIAliveCount++; - KFGT.RefreshMonsterAliveCount(); - NeedMoreZeds--; -} - -//Kill original boss -//function KillOriginalBoss() -//{ -// if(OriginalBoss!=None) -// { -// OriginalBoss.Suicide(); -// SetTimer(1, false, 'ResetCamera'); -// } -//} - -//Rollback camera mode -function ResetCamera() -{ - local KFPlayerController PC; - - foreach WorldInfo.AllControllers( class'KFPlayerController', PC ) - { - PC.ServerCamera( 'ThirdPerson' ); - PC.ServerCamera( 'FirstPerson' ); - PC.HideBossNameplate(); - } -} - -//Rollback wave number -function ReturnWaveNum() -{ - KFGT.MyKFGRI.WaveNum++; -} - -//Prevent end game on any but last boss kill -function bool PreventDeath(Pawn Killed, Controller Killer, class damageType, vector HitLocation) -{ - if(KFGT.WaveNum==KFGT.WaveMax) - { - if(UZBosses.Find(KFPawn_Monster(Killed))>=0) - { - BossesLeft--; - } - - if(KFPawn_MonsterBoss(Killed)!=None && BossesLeft>0) - { - KFGT.MyKFGRI.WaveNum--; - SetTimer(0.2, false, 'ReturnWaveNum'); - } - } - - return (NextMutator != None && NextMutator.PreventDeath(Killed, Killer, damageType, HitLocation)); -} - -function AddMutator(Mutator M) -{ - if( M!=Self ) - { - if( M.Class==Class ) - M.Destroy(); - else Super.AddMutator(M); - } -} - -//Spawn bosses by original boss -//function bool CheckReplacement(Actor Other) -//{ -// local int i, j; -// -// if(KFPawn_Monster(Other)!=None) -// NeedMoreZeds--; -// -// if(KFPawn_MonsterBoss(Other)!=None && !bFB && KFGT.WaveNum==KFGT.WaveMax) -// { -// bFB=true; -// OriginalBoss = KFPawn_Monster(Other); -// SetTimer(1,false,'KillOriginalBoss'); -// -// for( i=0; i MC ) -{ - local vector V; - local vector E,HL,HN; - local KFPawn_Monster M; - local Controller C; - - E.X = A.GetCollisionRadius()*0.8; - E.Y = E.X; - E.Z = A.GetCollisionHeight()*0.8; - V=A.Location; - V.Z+=32; //32 - - if(FRand()>0.5) - V.X+= FRand()>0.5 ? 100 : -100; - else - V.Y+= FRand()>0.5 ? 100 : -100; - - if( A.Trace(HL,HN,V,A.Location,false,E)!=None ) - V = HL; - - M = A.Spawn(MC,,,V,A.Rotation,,true); - if( M==None ) - return false; - C = M.Spawn(M.ControllerClass); - C.Possess(M,false); - - BossesLeft++; - UZBosses.AddItem(M); - - return true; -} - -DefaultProperties -{ - dt=1.0f -} diff --git a/builder.cfg b/builder.cfg new file mode 100644 index 0000000..66e3466 --- /dev/null +++ b/builder.cfg @@ -0,0 +1,52 @@ +### Build parameters ### + +# If True - compresses the mutator when compiling +# Scripts will be stored in binary form +# (reduces the size of the output file) +StripSource="True" + +# Mutators to be compiled +# Specify them with a space as a separator, +# Mutators will be compiled in the specified order +PackageBuildOrder="ZedSpawner" + + +### Steam Workshop upload parameters ### + +# Mutators that will be uploaded to the workshop +# Specify them with a space as a separator, +# The order doesn't matter +PackageUpload="ZedSpawner" + + +### Test parameters ### + +# Map: +Map="KF-Nuked" + +# Game: +# Survival: KFGameContent.KFGameInfo_Survival +# WeeklyOutbreak: KFGameContent.KFGameInfo_WeeklySurvival +# Endless: KFGameContent.KFGameInfo_Endless +# Objective: KFGameContent.KFGameInfo_Objective +# Versus: KFGameContent.KFGameInfo_VersusSurvival +Game="KFGameContent.KFGameInfo_Survival" + +# Difficulty: +# Normal: 0 +# Hard: 1 +# Suicide: 2 +# Hell: 3 +Difficulty="0" + +# GameLength: +# 4 waves: 0 +# 7 waves: 1 +# 10 waves: 2 +GameLength="0" + +# Mutators +Mutators="ZedSpawner.ZedSpawnerMut" + +# Additional parameters +Args="" diff --git a/tools b/tools new file mode 160000 index 0000000..49fcaf6 --- /dev/null +++ b/tools @@ -0,0 +1 @@ +Subproject commit 49fcaf67a29c5b478dc10f1f0ae08ed43017cd36