Class ScrnGameLength extends Object dependson(ScrnWaveInfo) PerObjectConfig Config(ScrnWaves); var ScrnGameType Game; var config string GameTitle; var config string Author; var config float BountyScale; var config int StartingCashBonus; var config array Mutators; var config array Waves; var config array Zeds; var config bool bLogStats; struct SHL { var byte Difficulty; var int HL; }; var config array HardcoreLevel; var class FallbackZed; var array ZedInfos; var array ZedVotes; struct SSpawnCandidate { var class ZedClass; var float Chance; }; struct SActiveZed { var string Alias; var array Candidates; var int WaveSpawns, TotalSpawns; }; var array ActiveZeds; // per-wave data struct SSquadMember { var int ActiveZedIndex; // item index in ActiveZeds array var int Count; }; struct SSquad { var byte MinPlayers; var byte MaxPlayers; var byte ScaleByPlayers; var array Members; }; var array Squads; var array SpecialSquads; var array PendingSquads; var array PendingSpecialSquads; var ScrnWaveInfo Wave; var transient int ZedsBeforeSpecial; var transient bool bLoadedSpecial; var transient float PlayerCountOverrideForHealth; var float WaveEndTime; var int WaveCounter; function LoadGame(ScrnGameType MyGame) { local int i, j; local ScrnZedInfo zi; local class zedc; Game = MyGame; for ( i = 0; i < Mutators.length; ++i ) { if ( Mutators[i] != "" ) { Log("Loading additional mutator: " $ Mutators[i], 'ScrnGameLength'); Game.AddMutator(Mutators[i], true); } } ZedInfos.length = Zeds.length; for ( i = 0; i < Zeds.length; ++i ) { zi = new(none, Zeds[i]) class'ScrnZedInfo'; for ( j = 0; j < zi.Zeds.length; ++j ) { if ( zi.Zeds[j].Vote != "" ) AddZedVote(zi.Zeds[j].Vote); if ( zi.Zeds[j].bDisabled ) continue; zedc = class(DynamicLoadObject(zi.Zeds[j].ZedClass, class'Class')); if ( zedc == none ) { log("Unable to load zed class '" $ zi.Zeds[j].ZedClass $ "' for " $ zi.Zeds[j].Alias, class.name); continue; } if ( zedc.outer.name != 'KFChar' ) { Game.AddToPackageMap(string(zedc.outer.name)); if ( zi.Zeds[j].Package != "" ) Game.AddToPackageMap(zi.Zeds[j].Package); } AddActiveZed(zi.Zeds[j].Alias, zedc, zi.Zeds[j].Pct); } // store zed info for config save after voting ZedInfos[i] = zi; } RecalculateSpawnChances(); for ( i = 0; i < ActiveZeds.length; ++i ) { for ( j = 0; j < ActiveZeds[i].Candidates.length; ++j ) { ActiveZeds[i].Candidates[j].ZedClass.static.PreCacheAssets(Game.Level); } } if ( ZedVotes.length > 0 ) AddVoting(); if ( HardcoreLevel.length > 0 ) { j = 0; for ( i = 0; i < HardcoreLevel.length; ++i ) { if ( Game.GameDifficulty >= HardcoreLevel[i].Difficulty && j < HardcoreLevel[i].HL ) j = HardcoreLevel[i].HL; } if ( j > 0 ) { log("HL forced to " $ j, class.name); Game.ScrnBalanceMut.GameRules.ForceHardcoreLevel(j); } } // this makes sure the Wave is never none if ( Waves.length == 0 ) { warn("ScrnGameLength: NO WAVES DEFINED!"); Wave = new(none, "Wave1") class'ScrnWaveInfo'; } else { Wave = CreateWave(Waves[0]); } } function AddVoting() { local ScrnVotingHandlerMut VH; local ScrnZedVoting VO; VH = class'ScrnVotingHandlerMut'.static.GetVotingHandler(Game); if ( VH != none ) { Game.AddMutator(string(class'ScrnVotingHandlerMut'), false); VH = class'ScrnVotingHandlerMut'.static.GetVotingHandler(Game); } if ( VH == none ) { log("Unable to spawn voting handler mutator", class.name); return; } VO = ScrnZedVoting(VH.AddVotingOptions(class'ScrnBalanceSrv.ScrnZedVoting')); if ( VO != none ) { VO.GL = self; } } // allows admins to control zed infos via MUTATE ZED function ZedCmd(PlayerController Sender, string cmd) { local array args; local int search_idx, cur_idx; // starts with 1 local int BoolValue; local int i, j; local bool bChanged; local color c; local float Pct; local bool bSetPct; BoolValue = -1; c.B = 1; Split(cmd, " ", args); if ( args.length == 0 || args[0] == "" ) { Sender.ClientMessage("MUTATE ZED LIST|( [ [ON|OFF] [PCT ]])"); return; } if ( args[0] ~= "LIST" ) { PrintAliases(Sender); return; } if ( args.length >= 2 ) { search_idx = int(args[1]); for ( i = 2; i < args.length - 1; ++i ) { if ( args[i] ~= "PCT" ) { bSetPct = true; Pct = float(args[i+1]); args.remove(i,2); break; } } if ( args.length >= 3 ) { BoolValue = class'ScrnVotingOptions'.static.TryStrToBoolStatic(args[2]); } } Sender.ClientMessage("INDEX / STATUS / SPAWN CHANCE / ZED CLASS"); Sender.ClientMessage("========================================================="); for ( i = 0; i < ZedInfos.length; ++i ) { bChanged = false; for ( j = 0; j < ZedInfos[i].Zeds.length; ++j ) { if ( ZedInfos[i].Zeds[j].Alias == args[0] ) { ++cur_idx; if ( ZedInfos[i].Zeds[j].bDisabled ) { c.R = 255; c.G = 1; } else { c.R = 1; c.G = 255; } if ( cur_idx == search_idx ) { if ( BoolValue != -1 ) { ZedInfos[i].Zeds[j].bDisabled = !bool(BoolValue); bChanged = true; } if ( bSetPct) { ZedInfos[i].Zeds[j].Pct = Pct; bChanged = true; } c.R = 255; c.G = 255; } Sender.ClientMessage(class'ScrnBalance'.static.ColorStringC( class'ScrnFunctions'.static.LPad(string(cur_idx), 3) @ class'ScrnFunctions'.static.RPad(eval(ZedInfos[i].Zeds[j].bDisabled, "OFF", "ON"), 5) @ class'ScrnFunctions'.static.LPad(eval(ZedInfos[i].Zeds[j].Pct == 0, "AUTO", string(ZedInfos[i].Zeds[j].Pct)), 7) @ ZedInfos[i].Zeds[j].ZedClass , c )); } } if ( bChanged ) ZedInfos[i].SaveConfig(); } if ( cur_idx == 0 ) { Sender.ClientMessage("No zeds with alias '"$args[0]$"' found!"); } } function PrintAliases(PlayerController Sender) { local array aliases; local array count; local int i, j, k; Sender.ClientMessage("Zed aliases (candidate count):"); for ( i = 0; i < ZedInfos.length; ++i ) { for ( j = 0; j < ZedInfos[i].Zeds.length; ++j ) { for ( k = 0; k < aliases.length; ++k ) { if ( ZedInfos[i].Zeds[j].Alias == aliases[k] ) break; } if ( k == aliases.length ) { aliases[k] = ZedInfos[i].Zeds[j].Alias; count[k] = 1; } else { ++count[k]; } } } for ( k = 0; k < aliases.length; ++k ) { Sender.ClientMessage(aliases[k] $ " ("$count[k]$")"); } } function bool LoadWave(int WaveNum) { local int i; local SSquad squad; local Controller C; local KFPlayerController KFPC; ZedsBeforeSpecial = 0; PendingSquads.length = 0; PendingSpecialSquads.length = 0; Squads.length = 0; SpecialSquads.length = 0; if ( bLogStats && Wave != none ) LogStats(); if ( WaveNum >= Waves.length ) { warn("ScrnGameLength: Illegal wave number: " $ WaveNum); if (Wave == none ) { log("Using fallback wave info", class.name); Wave = new(none, "Wave1") class'ScrnWaveInfo'; } return false; } Wave = CreateWave(Waves[WaveNum]); for ( i = 0; i < ActiveZeds.length; ++i ) { ActiveZeds[i].WaveSpawns = 0; } for ( i = 0; i < Wave.Squads.length; ++i ) { if ( ParseSquad(Wave.Squads[i], squad) ) { Squads[Squads.length] = squad; } } if ( Squads.length == 0 ) { log("No squads in wave", class.name); return false; } for ( i = 0; i < Wave.SpecialSquads.length; ++i ) { if ( ParseSquad(Wave.SpecialSquads[i], squad) ) { SpecialSquads[SpecialSquads.length] = squad; } } if ( Wave.MaxZombiesOnce > 0 ) Game.MaxZombiesOnce = Wave.MaxZombiesOnce; else Game.MaxZombiesOnce = Game.StandardMaxZombiesOnce; Game.ScrnGRI.bTraderArrow = Wave.bTraderArrow || Wave.bOpenTrader; Game.ScrnGRI.WaveEndRule = Wave.EndRule; Game.bZedPickupDosh = Wave.EndRule == RULE_GrabDoshZed; Game.bZedDropDosh = Wave.EndRule == RULE_GrabDoshZed || Wave.EndRule == RULE_GrabDosh; SetWaveInfo(); if ( Wave.TraderMessage != "" ) { for ( C = Game.Level.ControllerList; C != none; C = C.NextController ) { KFPC = KFPlayerController(C); if ( C.PlayerReplicationInfo != none && KFPC != none ) { // this is needed to show a trader portrait KFPC.ClientLocationalVoiceMessage(C.PlayerReplicationInfo, none, 'TRADER', 0); KFPC.TeamMessage(C.PlayerReplicationInfo, Wave.TraderMessage, 'TRADER'); } } } DoorControl(Wave.DoorControl); return true; } function ScrnWaveInfo CreateWave(string WaveDefinition) { local array Parts; local string WaveName; // get rid of spaces WaveName = Repl(WaveDefinition, " ", "", true); if ( InStr(WaveName, "|") != -1 ) { Split(WaveName, "|", Parts); WaveName = Parts[rand(Parts.length)]; } log("Creating wave " $ WaveName, class.name); return new(none, WaveName) class'ScrnWaveInfo'; } // Called by Game.SetupWave() when Battle Time starts (Trader/Cooldown Time ends) function RunWave() { local string s; SetWaveInfo(); DoorControl(Wave.DoorControl2); Game.ScrnGRI.bTraderArrow = Wave.bTraderArrow; if ( Wave.bRandomSpawnLoc ) { Game.ZedSpawnLoc = ZSLOC_RANDOM; } if ( Game.ScrnGRI.WaveTitle != "" || Game.ScrnGRI.WaveMessage != "" ) { if ( Game.ScrnGRI.WaveTitle != "" ) { s = Game.ScrnBalanceMut.ColorString(Game.ScrnGRI.WaveTitle, 255, 204, 1); } if ( Game.ScrnGRI.WaveMessage != "" ) { if ( s != "" ) s $= ": "; s $= Game.ScrnGRI.WaveMessage; } Game.Broadcast(Game, s); } } function SetWaveInfo() { if (Wave.PerPlayerMult == 0) { WaveCounter = Wave.Counter; switch ( Wave.EndRule ) { case RULE_KillEmAll: case RULE_SpawnEmAll: WaveCounter = Game.ScaleMonsterCount(WaveCounter); // apply default scaling } } else { WaveCounter = Wave.Counter * ( 1.0 + Wave.PerPlayerMult * (max(Game.AlivePlayerCount, Game.ScrnGRI.FakedPlayers) - 1) ); } if (Wave.MaxCounter > 0 && WaveCounter > Wave.MaxCounter) WaveCounter = Wave.MaxCounter; WaveEndTime = Game.Level.TimeSeconds + WaveCounter + 0.1; switch (Wave.EndRule) { case RULE_GrabAmmo: if ( Game.DesiredAmmoBoxCount < WaveCounter ) { Game.ScrnBalanceMut.AdjustAmmoBoxCount(min(WaveCounter, Game.AmmoPickups.length * 0.8)); } break; } Game.ScrnGRI.WaveHeader = Wave.Header; Game.ScrnGRI.WaveTitle = Wave.Title; Game.ScrnGRI.WaveMessage = Wave.Message; ReplaceText(Game.ScrnGRI.WaveMessage, "%c", string(WaveCounter)); Game.ScrnGRI.WaveCounter = 0; WaveTimer(); } function WaveTimer() { switch (Wave.EndRule) { case RULE_Timeout: Game.WaveEndTime = WaveEndTime; if ( Game.bWaveBossInProgress || Game.bWaveInProgress) { Game.ScrnGRI.TimeToNextWave = WaveEndTime - Game.Level.TimeSeconds; } break; case RULE_EarnDosh: case RULE_GrabDosh: case RULE_GrabDoshZed: Game.ScrnGRI.WaveCounter = max(0, WaveCounter - max(Game.Teams[0].Score, Game.Teams[1].Score)); break; case RULE_GrabAmmo: Game.ScrnGRI.WaveCounter = max(0, WaveCounter - Game.ScrnBalanceMut.GameRules.WaveAmmoPickups); break; } } // Called by ScrnGameType at the end of the wave - just before loading the next wave function WaveEnded() { local ScrnPlayerInfo SPI; if ( !(Wave.XP_Bonus ~= 0 && Wave.XP_BonusAlive ~= 0) ) { for ( SPI=Game.ScrnBalanceMut.GameRules.PlayerInfo; SPI!=none; SPI=SPI.NextPlayerInfo ) { if ( SPI.PlayerOwner == none || SPI.PlayerOwner.PlayerReplicationInfo == none ) continue; if ( SPI.bDied || Wave.XP_BonusAlive ~= 0 ) { SPI.BonusStats(SPI.GameStartStats, Wave.XP_Bonus); } else { SPI.BonusStats(SPI.GameStartStats, Wave.XP_BonusAlive); } } } } function DoorControl(ScrnWaveInfo.EDoorControl dc) { switch (dc) { case DOOR_Default: if (Game.ScrnBalanceMut.bRespawnDoors || Game.ScrnBalanceMut.bTSCGame) { Game.ScrnBalanceMut.RespawnDoors(); } break; case DOOR_Respawn: Game.ScrnBalanceMut.RespawnDoors(); break; case DOOR_Blow: Game.ScrnBalanceMut.BlowDoors(); break; case DOOR_Unweld: Game.ScrnBalanceMut.UnweldDoors(); break; case DOOR_UnweldRespawn: Game.ScrnBalanceMut.WeldDoors(0); break; case DOOR_Weld1p: Game.ScrnBalanceMut.WeldDoors(0.01); break; case DOOR_WeldHalf: Game.ScrnBalanceMut.WeldDoors(0.5); break; case DOOR_WeldFull: Game.ScrnBalanceMut.WeldDoors(1.0); break; case DOOR_WeldRandom: Game.ScrnBalanceMut.WeldDoors(-1.0); break; case DOOR_Randomize: Game.ScrnBalanceMut.RandomizeDoors(); break; } } function LogStats() { local int i, total; for ( i = 0; i < ActiveZeds.length; ++i ) total += ActiveZeds[i].WaveSpawns; if ( total == 0 ) return; log("Spawn Stats for " $ string(Wave.name) $ ":", class.name); for ( i = 0; i < ActiveZeds.length; ++i ) { if ( ActiveZeds[i].WaveSpawns > 0 ) log(ActiveZeds[i].Alias $ ": " $ ActiveZeds[i].WaveSpawns $ " / " $ string(ActiveZeds[i].WaveSpawns * 100.0 / total) $ "%"); } } function bool CheckWaveEnd() { local int i; switch ( Wave.EndRule ) { case RULE_KillEmAll: return Game.TotalMaxMonsters <= 0 && Game.NumMonsters <= 0; case RULE_SpawnEmAll: return Game.TotalMaxMonsters <= 0; case RULE_KillBoss: if ( Game.bBossSpawned && Game.Bosses.length > 0 ) { for ( i = 0; i < Game.Bosses.length; ++i ) { if ( Game.Bosses[i] != none && Game.Bosses[i].Health > 0 ) return false; // boss is alive } return true; // all bosses are dead } break; case RULE_Timeout: return Game.Level.TimeSeconds >= WaveEndTime; case RULE_EarnDosh: case RULE_GrabDosh: case RULE_GrabDoshZed: return max(Game.Teams[0].Score, Game.Teams[1].Score) >= WaveCounter; case RULE_GrabAmmo: return Game.ScrnBalanceMut.GameRules.WaveAmmoPickups >= WaveCounter; } // fallback scenario return Game.Level.TimeSeconds >= Game.WaveEndTime || (Game.TotalMaxMonsters <= 0 && Game.NumMonsters <= 0); } function int GetWaveZedCount() { switch ( Wave.EndRule ) { case RULE_KillBoss: return 1; case RULE_Timeout: case RULE_EarnDosh: case RULE_GrabDosh: case RULE_GrabDoshZed: case RULE_GrabAmmo: return 999; } return WaveCounter; } function float GetWaveEndTime() { switch ( Wave.EndRule ) { case RULE_Timeout: return WaveEndTime; } return Game.Level.TimeSeconds + 60; } function AdjustNextSpawnTime(out float NextSpawnTime) { NextSpawnTime /= Wave.SpawnRateMod; if ( bLoadedSpecial ) NextSpawnTime *= 2.0; // give players slight break after spawning a special squad } function LoadNextSpawnSquad(out array < class > NextSpawnSquad) { if ( ZedsBeforeSpecial <= 0 && SpecialSquads.length > 0 ) { LoadNextSpawnSquadInternal(NextSpawnSquad, SpecialSquads, PendingSpecialSquads, Wave.bRandomSquads); ZedsBeforeSpecial = Wave.ZedsPerSpecialSquad * (0.85 + 0.3*frand()); bLoadedSpecial = true; } else { LoadNextSpawnSquadInternal(NextSpawnSquad, Squads, PendingSquads, Wave.bRandomSpecialSquads); ZedsBeforeSpecial -= NextSpawnSquad.length; bLoadedSpecial = false; } } protected function LoadNextSpawnSquadInternal(out array < class > NextSpawnSquad, out array AllSquads, out array Pending, bool bRandom, optional int Recursions) { local int i, j, c, r; local int PlayerCount; PlayerCountOverrideForHealth = 0; PlayerCount = Game.GetPlayerCountForMonsterHealth(); if ( AllSquads.length == 0 ) { NextSpawnSquad.length = 1; NextSpawnSquad[0] = FallbackZed; return; } NextSpawnSquad.length = 0; if ( Pending.length == 0 ) { Pending.length = AllSquads.length; for ( i = 0; i < Pending.length; ++i ) { Pending[i] = i; } } if (bRandom) i = rand(Pending.length); else i = 0; r = Pending[i]; Pending.remove(i, 1); if ( Game.WavePlayerCount >= AllSquads[r].MinPlayers && (Game.WavePlayerCount <= AllSquads[r].MaxPlayers || AllSquads[r].MaxPlayers == 0) ) { for ( i = 0; i < AllSquads[r].Members.length; ++i ) { for ( j = 0; j < AllSquads[r].Members[i].Count; ++j ) { if (AllSquads[r].ScaleByPlayers == 0) { c = 1; } else { c = 1 + int( (PlayerCount - 0.5) / AllSquads[r].ScaleByPlayers ); PlayerCountOverrideForHealth = float(PlayerCount) / c; } while (c-- > 0) { NextSpawnSquad[NextSpawnSquad.length] = ActivateZed(AllSquads[r].Members[i].ActiveZedIndex); } } } } else if ( Recursions < 10 ){ // squad didn't passed player count restriction - pick another squad LoadNextSpawnSquadInternal(NextSpawnSquad, AllSquads, Pending, bRandom, Recursions + 1); } } function class ActivateZed(int idx) { local float r; local int i; ActiveZeds[idx].WaveSpawns++; ActiveZeds[idx].TotalSpawns++; if ( ActiveZeds[idx].Candidates.length == 1 ) return ActiveZeds[idx].Candidates[0].ZedClass; r = frand(); for ( i = 0; r > ActiveZeds[idx].Candidates[i].Chance && i < ActiveZeds[idx].Candidates.length - 1; ++i ) { r -= ActiveZeds[idx].Candidates[i].Chance; } return ActiveZeds[idx].Candidates[i].ZedClass; } function bool ParseSquad(string SquadDef, out SSquad Squad) { local int i, j, idx, count; local array parts; local string s, count_str, alias, fallback; Squad.Members.length = 0; Squad.MinPlayers = 0; Squad.MaxPlayers = 0; Squad.ScaleByPlayers = 0; // format example: 0-6: 2*CL/GF + BR/TH/HU + BL // another example: ~6: BOSS // get rid of spaces s = Repl(SquadDef, " ", "", true); Divide(s, ":", count_str, s); if ( count_str != "" ) { if ( Left(count_str, 1) == "~" ) { Squad.ScaleByPlayers = int(Mid(count_str, 1)); } else { Divide(count_str, "-", count_str, fallback); Squad.MinPlayers = int(count_str); Squad.MaxPlayers = int(fallback); } } Split(s, "+", parts); for ( i = 0; i < parts.length; ++i ) { s = parts[i]; do { if ( !Divide(s, "/", alias, fallback) ) { alias = s; fallback = ""; } s = alias; if ( !Divide(s, "*", count_str, alias) ) { count = 1; alias = s; } else { count = int(count_str); } idx = FindActiveZed(alias); s = fallback; } until ( idx != -1 || fallback == "" ); if ( idx == -1 ) { log("No zeds available to fit " $ parts[i] $ " in " $ SquadDef, class.name); Squad.Members.length = 0; return false; } else { j = Squad.Members.length; Squad.Members.insert(j, 1); Squad.Members[j].ActiveZedIndex = idx; Squad.Members[j].Count = count; } } return true; } function int FindActiveZed(string alias) { local int i; for ( i = 0; i < ActiveZeds.length; ++i ) { if ( ActiveZeds[i].Alias == alias ) return i; } log("Zed with alias '"$alias$"' not found", class.name); return -1; } function AddZedVote(string VoteString) { local int i; for ( i = 0; i < ZedVotes.length; ++i ) { if ( VoteString == ZedVotes[i] ) return; // already added else if ( VoteString < ZedVotes[i]) break; // sort alphabetically } ZedVotes.insert(i, 1); ZedVotes[i] = VoteString; } function AddActiveZed(string Alias, class ZedClass, optional float Chance) { local int i, j; for ( i = 0; i < ActiveZeds.length; ++i ) { if ( ActiveZeds[i].Alias == Alias ) break; } if ( i == ActiveZeds.length ) { ActiveZeds.insert(i, 1); ActiveZeds[i].Alias = Alias; } j = ActiveZeds[i].Candidates.length; ActiveZeds[i].Candidates.insert(j, 1); ActiveZeds[i].Candidates[j].ZedClass = ZedClass; ActiveZeds[i].Candidates[j].Chance = Chance; } // Recalculate spawn chances, removes records that have zero chance to spawn. function RecalculateSpawnChances() { local int i, j; local float chance; local int AutoChanceCount; for ( i = 0; i < ActiveZeds.length; ++i ) { if ( ActiveZeds[i].Candidates.length == 0 ) { log("Alias " $ ActiveZeds[i].Alias $ " has no zeds", class.name); ActiveZeds[i].Candidates.remove(i--, 1); continue; } chance = 0; AutoChanceCount = 0; for ( j = 0; j < ActiveZeds[i].Candidates.length; ++j ) { if ( ActiveZeds[i].Candidates[j].Chance == 0.0 ) ++AutoChanceCount; else chance += ActiveZeds[i].Candidates[j].Chance; } if ( AutoChanceCount > 0 ) { // split remaining change equally on remaining records chance = (1.0 - chance) / AutoChanceCount; for ( j = 0; j < ActiveZeds[i].Candidates.length; ++j ) { if ( ActiveZeds[i].Candidates[j].Chance == 0.0 ) { if ( chance > 0.0 ) ActiveZeds[i].Candidates[j].Chance = chance; else ActiveZeds[i].Candidates.remove(j--, 1); } } } else if ( abs(chance) - 1.0 > 0.01 ) { // modify all records to have total chance = 1.0 chance = 1.0 / chance; for ( j = 0; j < ActiveZeds[i].Candidates.length; ++j ) { ActiveZeds[i].Candidates[j].Chance *= chance; } } } } function bool ShouldBoostAmmo() { return Wave != none && Wave.EndRule == RULE_GrabAmmo; } function float GetBountyScale() { if (Wave.BountyScale > 0) return Wave.BountyScale; return BountyScale; } defaultproperties { BountyScale=1.000000 Waves(0)="Wave1" Zeds(0)="NormalZeds" bLogStats=True FallbackZed=Class'KFChar.ZombieClot_STANDARD' }