class TSCGame extends ScrnGameType config; var TSCGameReplicationInfo TSCGRI; var TSCVotingOptions TSCVotingOptions; var int OriginalFinalWave; var localized string RedTeamHumanName, BlueTeamHumanName; var config byte OvertimeWaves; // number of Overtime waves var config byte SudDeathWaves; // number of Sudden Death waves var config float OvertimeTeamMoneyPenalty; // team receives less and less money for each subsequent overtime wave var config int LateJoinerCashBonus; // extra money for late joiners to prevent, for example, joining before wave 100 with only $100 cash var config string SongRedWin, SongBlueWin, SongBothWin, SongBothWiped; var float CurrentTeamMoneyPenalty; var localized string strBudgetCut; var TSCBaseGuardian TeamBases[2]; var float BaseRadius; // Base radius var float MinBaseZ, MaxBaseZ; // min and max Z difference between player and base var transient int BigTeamSize, SmallTeamSize; // number of alive players in biggest team at start of the wave var transient bool bSingleTeam; // wave started without other team var transient bool bTeamWiped; // one of the team was wiped out during the wave. Always true when bSingleTeam=True var transient bool bWaveEnding; // indicates end phase of the wave. Always must be checked together with bWaveBossInProgress! var int WaveEndingCountdown; // if bWaveEnding=True, shows how many seconds left until auto-end wave (auto-kill remaining zeds) var byte NextSquadTeam; var bool bCheckSquadTeam; // should NextSquadTeam be checked in FindSquadTarget()? var array < class > PendingSpecialSquad; var TSCTeam TSCTeams[2]; var class BaseGuardianClasses[2]; var ShopVolume TeamShops[2]; var class BaseWhisp; var bool bPendingShuffle; // shuffle teams at the end of the wave var protected bool bTeamChanging; // indicates that game changes team members, e.g. doing shuffle var config bool bLockTeamsOnSuddenDeath; struct SClanTags { var config string Prefix, Postfix; }; var config array ClanTags; var config bool bClanCheck; var bool bCustomHUD, bCustomScoreboard; var bool bPvP, bNoBases; var bool bCtryTags; // indicates if Country Tags mutator is running var enum EHumanDamageMode { HDMG_None, HDMG_NoFF, HDMG_NoFF_PvP, HDMG_Normal, HDMG_PvP, HDMG_All, } HumanDamageMode; var config bool bVoteHDmg, bVoteHDmgOnlyBeforeStart; var float HDmgScale; // this one is called from PreBeginPlay() function InitGameReplicationInfo() { Super.InitGameReplicationInfo(); TSCGRI = TSCGameReplicationInfo(GameReplicationInfo); if ( TSCGRI == none ) Warn("Wrong GameReplicationInfo class: " $ GameReplicationInfo); else { TSCGRI.OvertimeWaves = OvertimeWaves; TSCGRI.SudDeathWaves = SudDeathWaves; TSCGRI.BaseRadiusSqr = BaseRadius * BaseRadius; TSCGRI.MinBaseZ = MinBaseZ; TSCGRI.MaxBaseZ = MaxBaseZ; TSCGRI.HumanDamageMode = HumanDamageMode; TSCGRI.bSingleTeamGame = bSingleTeamGame; } } event PreBeginPlay() { Super.PreBeginPlay(); GameReplicationInfo.bNoTeamSkins = bSingleTeamGame; GameReplicationInfo.bNoTeamChanges = bSingleTeamGame; } function PostBeginPlay() { super.PostBeginPlay(); TSCTeams[0] = TSCTeam(Teams[0]); TSCTeams[1] = TSCTeam(Teams[1]); SpawnBaseGuardian(0); Teams[0].HomeBase = TeamBases[0]; Teams[0].ColorNames[0]= RedTeamHumanName; Teams[0].ColorNames[1]= BlueTeamHumanName; Teams[0].TeamColor=class'Canvas'.static.MakeColor(180, 0, 0, 255); SpawnBaseGuardian(1); Teams[1].HomeBase = TeamBases[1]; Teams[1].ColorNames[0]= RedTeamHumanName; Teams[1].ColorNames[1]= BlueTeamHumanName; Teams[1].TeamColor=class'Canvas'.static.MakeColor(32, 92, 255, 255); } event InitGame( string Options, out string Error ) { local ScrnVotingHandlerMut VH; local Mutator M; ScrnBalanceMut.bScrnWaves = true; super.InitGame(Options, Error); // check loaded mutators for (M = BaseMutator; M != None; M = M.NextMutator) { bCtryTags = bCtryTags || M.IsA('CtryTags'); } // hard-code some ScrN features ScrnBalanceMut.MaxVoteKillMonsters = 0; // no vote end wave since it is auto-ended in 30 seconds ScrnBalanceMut.bSpawn0 = true; ScrnBalanceMut.bNoStartCashToss = true; ScrnBalanceMut.bMedicRewardFromTeam = true; if ( ScrnBalanceMut.ForcedMaxPlayers < 12 ) ScrnBalanceMut.ForcedMaxPlayers = 0; VH = class'ScrnVotingHandlerMut'.static.GetVotingHandler(self); if ( VH != none ) { TSCVotingOptions = TSCVotingOptions(VH.AddVotingOptions(class'TSCVotingOptions')); if ( TSCVotingOptions != none ) { TSCVotingOptions.TSC = self; } else log("!!! Unable to spawn TSCVotingOptions!", 'TSC'); } else log("Voting (mvote) disabled.", 'TSC'); bUseEndGameBoss = false; FinalWave = max(GetIntOption(Options, "NWaves", FinalWave), 1); OriginalFinalWave = FinalWave; OvertimeWaves = max(GetIntOption(Options, "OTWaves", OvertimeWaves), 0); SudDeathWaves = max(GetIntOption(Options, "SDWaves", SudDeathWaves), 0); // force FriendlyFireScale to 10% FriendlyFireScale = HDmgScale; default.FriendlyFireScale = HDmgScale; // set MaxZombiesOnce to at least 48, unless it is a small map with only 1 trader if ( MaxZombiesOnce < 48 && !bSingleTeamGame && ShopList.Length > 1 ) { MaxZombiesOnce = 48; } // todo - allow mutators to alter those settings if ( !bCustomHUD ) HUDType = string(Class'ScrnBalanceSrv.TSCHUD'); if ( !bCustomScoreboard ) ScoreBoardType = string(Class'ScrnBalanceSrv.TSCScoreBoard'); } //force using DefaultEnemyRosterClass function UnrealTeamInfo GetBlueTeam(int TeamBots) { local class RosterClass; local UnrealTeamInfo Roster; if ( DefaultEnemyRosterClass != "" ) { RosterClass = class(DynamicLoadObject(DefaultEnemyRosterClass,class'Class')); if ( RosterClass != none ) Roster = spawn(RosterClass); } if ( Roster == none ) Roster = super.GetBlueTeam(0); return Roster; } function UnrealTeamInfo GetRedTeam(int TeamBots) { local class RosterClass; local UnrealTeamInfo Roster; if ( DefaultEnemyRosterClass != "" ) { RosterClass = class(DynamicLoadObject(DefaultEnemyRosterClass,class'Class')); if ( RosterClass != none ) Roster = spawn(RosterClass); } if ( Roster == none ) Roster = super.GetRedTeam(0); return Roster; } // extracts ClanName from PlayerName or returns empty string, if player name // doesn't contain clan. function string ClanName(string PlayerName) { local int i, namelen, pos; local string S; if ( PlayerName == "" ) return ""; PlayerName = ScrnBalanceMut.StripColorTags(PlayerName); if ( bCtryTags && Mid(PlayerName,0,1)=="[" ) { // do not use country tags as clan names if( Mid(PlayerName,4,1)=="]" ) PlayerName = Mid(PlayerName,5); // 3-char country code else if( Mid(PlayerName,3,1)=="]" ) PlayerName = Mid(PlayerName,4); // 2-char country code } namelen = len(PlayerName); for ( i=0; i 0 ) return Left(S, pos); } } return ""; } // returns team, were same clan members as MyPRI are playing // returns none if no same clan memebers are on the server function UnrealTeamInfo MyClanTeam(PlayerReplicationInfo myPRI) { local int i; local PlayerReplicationInfo PRI; local string MyClan; MyClan = ClanName(myPRI.PlayerName); if ( MyClan == "" ) return none; // debug Broadcast(Self, ScrnBalanceMut.ColoredPlayerName(myPRI)$" associated with '"$MyClan$"' clan"); for ( i=0; i BigTeam.Score ) { SmallTeam = BigTeam; BigTeam = Teams[1-SmallTeam.TeamIndex]; } if ( num < 2 ) NewTeam = Teams[num]; else NewTeam = SmallTeam; if ( bPlayersBalanceTeams && (Level.NetMode != NM_Standalone) && (PlayerController(C) != None) ) { if ( SmallTeam.Size < BigTeam.Size ) NewTeam = SmallTeam; } return NewTeam.TeamIndex; } // shuffling only human players, without touching bots // todo: or not to do? Bot support. function ShuffleTeams() { local PlayerReplicationInfo PRI; local array RedPlayers, BluePlayers; local int ConstantReds, ConstantBlues; // amount of player which can't change the team, e.g. because of carrying the gnome local int i, RedTeamSize; if ( bSingleTeamGame ) return; // no team game = no shuffle if ( GameReplicationInfo.bMatchHasBegun ) { if ( bWaveInProgress ) { bPendingShuffle = true; BroadcastLocalizedMessage(class'TSCMessages', 240); return; } else if ( WaveCountDown < 10 ) { return; } else if ( WaveCountDown < 30 ) { WaveCountDown += 30; TSCGRI.TimeToNextWave = WaveCountDown; } } bPendingShuffle = false; for ( i = 0; i < GameReplicationInfo.PRIArray.Length; ++i ) { PRI = GameReplicationInfo.PRIArray[i]; if ( PRI != none && !PRI.bOnlySpectator && PRI.Team != none && PRI.Team.TeamIndex < 2 && PlayerController(PRI.Owner) != none ) { if ( PRI.HasFlag == none ) RedPlayers[RedPlayers.Length] = PRI; else if ( PRI.Team.TeamIndex == 0 ) ++ConstantReds; // can't move red gnome carrier else ++ConstantBlues; // can't move blue gnome carrier } } // now all players are in RedPlayers array. Move half to blue. RedTeamSize = RedPlayers.length - ConstantReds + ConstantBlues; RedTeamSize = max(0, RedTeamSize/2 + (RedTeamSize%2) * rand(2)); //Broadcast(Self, "TotalPlayers="$RedPlayers.length @ "RedTeamSize="$RedTeamSize); //Broadcast(Self, "ConstantReds="$ConstantReds @ "ConstantBlues="$ConstantBlues); while ( RedPlayers.length > RedTeamSize ) { i = rand(RedPlayers.length); BluePlayers[BluePlayers.length] = RedPlayers[i]; RedPlayers.remove(i, 1); } //Broadcast(Self, "RedPlayers="$RedPlayers.length @ "BluePlayers="$BluePlayers.length); bTeamChanging = true; for ( i=0; i < RedPlayers.length; ++i ) { if ( RedPlayers[i].Team.TeamIndex != 0 ) PlayerController(RedPlayers[i].Owner).ServerChangeTeam(0); } for ( i=0; i < BluePlayers.length; ++i ) { if ( BluePlayers[i].Team.TeamIndex != 1 ) PlayerController(BluePlayers[i].Owner).ServerChangeTeam(1); } bTeamChanging = false; BroadcastLocalizedMessage(class'TSCMessages', 241); } function SetTeamCaptain(byte TeamIndex, PlayerReplicationInfo NewCaptainPRI) { if ( TeamIndex > 1 ) return; if ( NewCaptainPRI != none && (NewCaptainPRI.Team == none || NewCaptainPRI.Team.TeamIndex != TeamIndex) ) return; TSCGRI.TeamCaptain[TeamIndex] = NewCaptainPRI; if ( NewCaptainPRI == none ) TSCVotingOptions.VotingHandler.TeamCaptains[TeamIndex] = none; else TSCVotingOptions.VotingHandler.TeamCaptains[TeamIndex] = PlayerController(NewCaptainPRI.Owner); } function bool ShouldKillOnTeamChange(Pawn TeamChanger) { if ( bTeamChanging ) return false; // do not kill pawn, if changing to a smaller team if ( !bWaveInProgress && TeamChanger.PlayerReplicationInfo != none && TeamChanger.PlayerReplicationInfo.Team != none && TeamChanger.PlayerReplicationInfo.Team.TeamIndex < 2 && TeamChanger.PlayerReplicationInfo.Team.Size <= Teams[1-TeamChanger.PlayerReplicationInfo.Team.TeamIndex].Size ) { return false; } return true; } function int CalcStartingCashBonus(PlayerController PC) { local int result; result = super.CalcStartingCashBonus(PC); if ( WaveNum > 0 && LateJoinerCashBonus > 0 && !PC.PlayerReplicationInfo.bOnlySpectator && PC.PlayerReplicationInfo.Team != none && PC.PlayerReplicationInfo.Team.TeamIndex < 2 && PC.PlayerReplicationInfo.Team.Size <= BigTeamSize && PC.PlayerReplicationInfo.Team.Size <= Teams[1-PC.PlayerReplicationInfo.Team.TeamIndex].Size ) { result += WaveNum * LateJoinerCashBonus/OriginalFinalWave * (1.0 - CurrentTeamMoneyPenalty); } return result; } function bool BecomeSpectator(PlayerController P) { if ( super.BecomeSpectator(P) ) { // check if player is a team captain if ( P.PlayerReplicationInfo.Team != none && P.PlayerReplicationInfo.Team.TeamIndex < 2 ) { if ( TSCGRI.TeamCaptain[P.PlayerReplicationInfo.Team.TeamIndex] == P.PlayerReplicationInfo ) { SetTeamCaptain(P.PlayerReplicationInfo.Team.TeamIndex, none); } } return true; } return false; } function SetHumanDamage(EHumanDamageMode Hdmg) { HumanDamageMode = Hdmg; TSCGRI.HumanDamageMode = Hdmg; } /** * Returns true, if friendly fire is disabled. To be precise, FFDisabled() always returns true, * if Victim can not be hurt by other player, even from other team. * * @param instigatedBy player pawn, which made damage to Victim * @param Victim player pawn, which is hurt by instigatedBy * @return true, if Victim shouldn't take damage from other player */ function bool FFDisabled(pawn instigatedBy, pawn Victim) { if ( bTradingDoorsOpen || WaveNum == 0 || HumanDamageMode == HDMG_None || (!bWaveBossInProgress && TotalMaxMonsters<=0 && NumMonsters < 10) ) return true; if ( HumanDamageMode == HDMG_All ) return false; if ( instigatedBy.GetTeamNum() == Victim.GetTeamNum() ) { return HumanDamageMode < HDMG_Normal || TSCGRI.AtOwnBase(Victim); } return HumanDamageMode < HDMG_PvP && TSCGRI.AtOwnBase(Victim); } function int ReduceDamage(int Damage, pawn injured, pawn instigatedBy, vector HitLocation, out vector Momentum, class DamageType) { if ( Damage == 0 ) return 0; if( instigatedBy!=injured && KFHumanPawn(injured) != none && instigatedBy != none && MonsterController(InstigatedBy.Controller)==None ) { // player to player damage if ( FFDisabled(instigatedBy, injured) ) return 0; // same team friendly fire damage will be reduced in KFGameType if ( instigatedBy.GetTeamNum()!=injured.GetTeamNum() ) Damage = max(1, round( Damage * FriendlyFireScale ) ); } return super.ReduceDamage(Damage, injured, instigatedBy, HitLocation, Momentum, DamageType); } function Killed(Controller Killer, Controller Killed, Pawn KilledPawn, class damageType) { local TeamInfo KilledTeam; local bool bHadMonsters; local byte Ping; // ping is multiplied by 4 in KF! local int HealthBeforeDeath; local bool bSuicide; bHadMonsters = NumMonsters > 0; // save team here in cases when Killed destroyed by parent function if ( Killed.bIsPlayer && Killed.PlayerReplicationInfo != none ) { KilledTeam = Killed.PlayerReplicationInfo.Team; Ping = Killed.PlayerReplicationInfo.Ping; bSuicide = Killer == Killed && (damageType == class'Suicided' || damageType == class'DamageType'); if ( ScrnHumanPawn(KilledPawn) != none ) HealthBeforeDeath = ScrnHumanPawn(KilledPawn).HealthBeforeDeath; } super.Killed(Killer, Killed, KilledPawn, damageType); if ( KilledTeam != none ) { AliveTeamPlayerCount[KilledTeam.TeamIndex]--; //Broadcast(Self, "HealthBeforeDeath="$HealthBeforeDeath); // During Sudden Death wipe team after player's death, unless: // - he isn't fully joined yet or lost the connection (ping == 255) // - he isn't suicided/disconnected. To prevent cheating, only players with full health are allowed to suicide if ( bWaveInProgress && Ping < 255 && damageType != class'DamTypeSudDeath' && (!bSuicide || HealthBeforeDeath < 80 ) ) { TSCTeam(KilledTeam).Deaths++; if ( TSCGRI.bSuddenDeath ) WipeTeam(KilledTeam); } if ( !bTeamWiped && AliveTeamPlayerCount[KilledTeam.TeamIndex] == 0) { bTeamWiped = true; // lower amount of remaining but not-spawned zeds twice if ( TotalMaxMonsters > 1) { TotalMaxMonsters /= 2; TSCGRI.MaxMonsters = Max(TotalMaxMonsters + NumMonsters,0); } BroadcastLocalizedMessage(class'TSCMessages', 10+KilledTeam.TeamIndex*100); TeamBases[KilledTeam.TeamIndex].SendHome(); } } else if ( NumMonsters == 0 && bHadMonsters && !bSingleTeam && damageType != class'Suicided' ) { if ( AliveTeamPlayerCount[0] == 0 ^^ AliveTeamPlayerCount[1] == 0 ) DramaticEvent(1.0); // do ZED time on winner's kill } } function ScoreKill(Controller Killer, Controller Other) { super.ScoreKill(Killer, Other); if ( KFMonsterController(Other) != none && Killer != none && Killer.PlayerReplicationInfo != None && TSCTeam(killer.PlayerReplicationInfo.Team) != none ) { TSCTeam(Killer.PlayerReplicationInfo.Team).ZedKills++; } } function WipeTeam(TeamInfo Team, optional class DamageType) { local Controller C; local Vector NullVector; if ( DamageType == none ) DamageType = class'DamTypeSudDeath'; for ( C = Level.ControllerList; C != none; C = C.nextController ) { if ( C.bIsPlayer && C.Pawn != none && C.Pawn.Health > 0 && C.PlayerReplicationInfo != none && C.PlayerReplicationInfo.Team == Team ) { C.Pawn.Died(none, DamageType, NullVector); } } } function bool CheckEndGame(PlayerReplicationInfo Winner, string Reason) { local Controller P; local PlayerController Player; local bool bSetAchievement; local string MapName; local String EndSong; if ( Reason == "TeamScoreLimit" ) { if ( AliveTeamPlayerCount[0] > 0 && AliveTeamPlayerCount[1] == 0 ) { TSCGRI.Winner = Teams[0]; EndSong = SongRedWin; } else if ( AliveTeamPlayerCount[0] == 0 && AliveTeamPlayerCount[1] > 0 ) { TSCGRI.Winner = Teams[1]; EndSong = SongBlueWin; } else return false; TSCGRI.EndGameType = 2; ScrnBalanceMut.BroadcastMessage(TeamInfo(TSCGRI.Winner).GetHumanReadableName() $ " team won the game on wave " $ string(WaveNum+1), true); } else { if ( WaveNum >= OriginalFinalWave + OvertimeWaves + SudDeathWaves ) { TSCGRI.Winner = none; TSCGRI.EndGameType = 2; EndSong = SongBothWin; if ( GameDifficulty >= 2.0 ) { bSetAchievement = true; // Get the MapName out of the URL MapName = GetCurrentMapName(Level); } } else { TSCGRI.EndGameType = 1; EndSong = SongBothWiped; } } if ( (GameRulesModifiers != None) && !GameRulesModifiers.CheckEndGame(Winner, Reason) ) { TSCGRI.EndGameType = 0; TSCGRI.Winner = none; return false; } // if we reached here, game must be ended EndTime = Level.TimeSeconds + EndTimeDelay; for ( P = Level.ControllerList; P != none; P = P.nextController ) { Player = PlayerController(P); if ( Player != none ) { Player.ClientSetBehindView(true); Player.ClientGameEnded(); if ( bSetAchievement && Player.PlayerReplicationInfo != none && KFSteamStatsAndAchievements(Player.SteamStatsAndAchievements) != none && (Player.PlayerReplicationInfo.Team == GameReplicationInfo.Winner || GameReplicationInfo.Winner == none) ) { KFSteamStatsAndAchievements(Player.SteamStatsAndAchievements).WonGame(MapName, GameDifficulty, KFGameLength == GL_Long); } if (KFPlayerController(Player)!= none) KFPlayerController(Player).NetPlayMusic(EndSong, 0.5, 0); } P.GameHasEnded(); } if ( CurrentGameProfile != none ) CurrentGameProfile.bWonMatch = false; return true; } function TSCBaseGuardian SpawnBaseGuardian(byte TeamIndex) { local NavigationPoint N; local TSCBaseGuardian gnome; if ( TeamIndex >= 2 ) return none; if ( TeamBases[TeamIndex] != none ) return TeamBases[TeamIndex]; // already spawned gnome = spawn(BaseGuardianClasses[TeamIndex], TSCGRI); if ( gnome == none ) { // if unable to spawn at zero point, try player spawn N = FindPlayerStart(none, TeamIndex); if ( N != none ) gnome = spawn(BaseGuardianClasses[TeamIndex], TSCGRI, '', N.Location); } if ( gnome == none ) { // realy? can't spawn gnome at player spawn?! N = Level.NavigationPointList; while ( gnome == none && N != none ) { gnome = spawn(BaseGuardianClasses[TeamIndex], TSCGRI, '', N.Location); N = N.nextNavigationPoint; } } if ( gnome == none ) { // map is totally fucked up log("Unable to spawn Base Guardian for team " $ TeamIndex, 'TSC'); return none; } gnome.Team = Teams[TeamIndex]; gnome.TSCGRI = TSCGRI; TeamBases[TeamIndex] = gnome; return gnome; } // balances the team referenced by SmallTeamIndex // BalanceMult shows the relatice amount of bonus that must be given to small team // Usually BalanceMult=BigTeam.Size / SmallTeam.Size // callint BalanceTeam(2, 1.0) removes any previously given bonuses function BalanceTeams(byte SmallTeamIndex, float BalanceMult) { local Controller C; local Inventory Inv; local Syringe S; local ScrnHumanPawn ScrnPawn; local bool bInSmallTeam; if ( bSingleTeamGame ) return; for ( C = Level.ControllerList; C != none; C = C.nextController ) { if ( !C.bIsPlayer || C.PlayerReplicationInfo == none || C.Pawn == none ) continue; ScrnPawn = ScrnHumanPawn(C.Pawn); if ( ScrnPawn == none ) continue; bInSmallTeam = C.PlayerReplicationInfo.Team != none && C.PlayerReplicationInfo.Team.TeamIndex == SmallTeamIndex; ScrnPawn.HealthBonus = 0; if ( bInSmallTeam ) ScrnPawn.HealthBonus *= BalanceMult; // increases max health ScrnPawn.GiveHealth(0, 100); // updates max health for ( Inv = ScrnPawn.Inventory; Inv != none; Inv = Inv.Inventory ) { S = Syringe(Inv); if ( S != none ) { S.HealBoostAmount = S.default.HealBoostAmount; // give more healing points to smaller team if ( bInSmallTeam ) S.HealBoostAmount *= BalanceMult; } } } } // returns wave number relative to the current game length function byte RelativeWaveNum(float LongGameWaveNum) { if ( OriginalFinalWave == 10 ) return ceil(LongGameWaveNum); return ceil(LongGameWaveNum * OriginalFinalWave / 10.0); } function SetupWave() { local int i; bWaveInProgress = true; TSCGRI.bWaveInProgress = true; if ( (WaveNum+1) == RelativeWaveNum(ScrnBalanceMut.LockTeamAutoWave) ) LockTeams(); NextMonsterTime = Level.TimeSeconds + 5.0; TraderProblemLevel = 0; rewardFlag=false; ZombiesKilled=0; WaveMonsters = 0; WaveNumClasses = 0; bWaveEnding = false; SetupPickups(); i = rand(2); TeamBases[i].ScoreOrHome(); TeamBases[1-i].ScoreOrHome(); WavePlayerCount = AlivePlayerCount; BigTeamSize = max(AliveTeamPlayerCount[0], AliveTeamPlayerCount[1]); SmallTeamSize = min(AliveTeamPlayerCount[0], AliveTeamPlayerCount[1]); bSingleTeam = AliveTeamPlayerCount[0] == 0 || AliveTeamPlayerCount[1] == 0; bTeamWiped = bSingleTeam; if ( bSingleTeam || AliveTeamPlayerCount[0] == AliveTeamPlayerCount[1] ) { BalanceTeams(2, 1.0); // remove bonuses SmallTeamSize = BigTeamSize; } else if ( AliveTeamPlayerCount[0] < AliveTeamPlayerCount[1] ) BalanceTeams(0, float(AliveTeamPlayerCount[1])/AliveTeamPlayerCount[0]); else if ( AliveTeamPlayerCount[0] > AliveTeamPlayerCount[1] ) BalanceTeams(1, float(AliveTeamPlayerCount[0])/AliveTeamPlayerCount[1]); ScrnGameLength.RunWave(); CalcDoshDifficultyMult(); if( WaveNum == FinalWave && bUseEndGameBoss ) { StartWaveBoss(); return; } if ( WaveNum == 0 ) { BroadcastLocalizedMessage(class'TSCMessages', 230); // human damage disabled } else if ( WaveNum >= OriginalFinalWave ) { if ( WaveNum >= OriginalFinalWave + OvertimeWaves ) { TSCGRI.bSuddenDeath = true; BroadcastLocalizedMessage(class'TSCMessages', 302); // sudden death // if one of the team do not have a base - wipe it } else { BroadcastLocalizedMessage(class'TSCMessages', 201); // overtime // lock teams one wave before Sudden Death if ( bLockTeamsOnSuddenDeath && SudDeathWaves > 0 && WaveNum+1 == OriginalFinalWave + OvertimeWaves ) LockTeams(); } } else if ( HumanDamageMode > HDMG_None ) { if ( HumanDamageMode >= HDMG_Normal ) BroadcastLocalizedMessage(class'TSCMessages', 231); // human damage enabled else BroadcastLocalizedMessage(class'TSCMessages', 232); // enemy fire enabled } TotalMaxMonsters = ScrnGameLength.GetWaveZedCount() + NumMonsters; WaveEndTime = ScrnGameLength.GetWaveEndTime(); AdjustedDifficulty = GameDifficulty + lerp(float(WaveNum)/FinalWave, 0.1, 0.3); MaxMonsters = min(TotalMaxMonsters, MaxZombiesOnce); // max monsters that can be spawned TSCGRI.MaxMonsters = TotalMaxMonsters; // num monsters in wave replicated to clients TSCGRI.MaxMonstersOn = true; // I've no idea what is this for NextSquadTeam = rand(2); // pickup random team for the next special squad for( i=0; i CL; local Controller C; for( C=Level.ControllerList; C!=None; C=C.NextController ) { if ( C.bIsPlayer && C.Pawn!=None && C.Pawn.Health>0 && (bTeamWiped || !bCheckSquadTeam || C.GetTeamNum() == NextSquadTeam) ) CL[CL.Length] = C; } if( CL.Length>0 ) return CL[Rand(CL.Length)]; return super.FindSquadTarget(); // in case when there are no team players alive } protected function StartTourney() { super.StartTourney(); GameReplicationInfo.bNoTeamSkins = bSingleTeamGame; OvertimeTeamMoneyPenalty = 0.50; LateJoinerCashBonus = 0; bAntiBlocker = true; bVoteHDmg = false; } function bool RewardSurvivingPlayers() { // At this WaveNum isn't increased yet if ( OvertimeTeamMoneyPenalty > 0 && WaveNum+1 >= OriginalFinalWave ) { CurrentTeamMoneyPenalty = fmin(CurrentTeamMoneyPenalty + OvertimeTeamMoneyPenalty, 1); Teams[0].Score = int(Teams[0].Score * (1.0 - CurrentTeamMoneyPenalty)); Teams[1].Score = int(Teams[1].Score * (1.0 - CurrentTeamMoneyPenalty)); Broadcast(Self, strBudgetCut @ int(100*(CurrentTeamMoneyPenalty)) $ "%"); } return super.RewardSurvivingPlayers(); } function ShowPathTo(PlayerController P, int DestinationIndex) { if ( DestinationIndex == 1 ) ShowPathToBase(P); else super.ShowPathTo(P, DestinationIndex); // show path to shop } function ShowPathToBase(PlayerController P) { local TSCBaseGuardian gnome; local byte TeamNum; TeamNum = P.PlayerReplicationInfo.Team.TeamIndex; gnome = TeamBases[TeamNum]; if ( gnome == none || !gnome.bActive || TSCGRI.AtOwnBase(P.Pawn) ) { ScrnPlayerController(P).ServerShowPathTo(255); // turn off return; } if ( P.FindPathToward(gnome, false) != None ) { Spawn(BaseWhisp, P,, P.Pawn.Location); } } function ShopVolume TeamShop(byte TeamIndex) { switch (TeamIndex) { case 0: return TSCGRI.CurrentShop; case 1: return TSCGRI.BlueShop; } return none; } State MatchInProgress { function WaveTimer() { super.WaveTimer(); if ( TotalMaxMonsters <= 0 ) { if ( NumMonsters > 0 && NumMonsters < 10 ) { if ( bWaveEnding ) { if ( --WaveEndingCountDown <= 0) { KillRemainingZeds(true); bWaveEnding = false; } // tell about disabling Human Damage 3 seconds after auto-end message if ( HumanDamageMode > HDMG_None && WaveEndingCountDown == default.WaveEndingCountDown-3 ) BroadcastLocalizedMessage(class'TSCMessages', 230); } else { bWaveEnding = true; WaveEndingCountDown = default.WaveEndingCountDown; // force end wave in 30 seconds BroadcastLocalizedMessage(class'TSCMessages', 200); // tell about auto-end } } } if ( (int(WaveTimeElapsed)%60) == 0 ) { TSCTeams[0].PrevMinKills = TSCTeams[0].LastMinKills; TSCTeams[0].LastMinKills = TSCTeams[0].ZedKills; TSCTeams[1].PrevMinKills = TSCTeams[1].LastMinKills; TSCTeams[1].LastMinKills = TSCTeams[1].ZedKills; } } function DoWaveEnd() { local int NextWave; NextWave = WaveNum + 1; if ( !bSingleTeam && (AliveTeamPlayerCount[0] == 0 ^^ AliveTeamPlayerCount[1] == 0) ) { EndGame(None,"TeamScoreLimit"); } else if ( AlivePlayerCount > 0 && NextWave >= OriginalFinalWave ) { if ( NextWave >= OriginalFinalWave + OvertimeWaves + SudDeathWaves ) { WaveNum++; EndGame(None,"TimeLimit"); return; } if ( OvertimeWaves > 0 && NextWave == OriginalFinalWave ) { BroadcastLocalizedMessage(class'TSCMessages', 201); } else if ( NextWave >= OriginalFinalWave + OvertimeWaves ) { TSCGRI.bSuddenDeath = true; BroadcastLocalizedMessage(class'TSCMessages', 202); } FinalWave++; } if ( !bSingleTeamGame ) { if ( TeamBases[0] == none ) SpawnBaseGuardian(0); // just in case // don't check for none here making warning message appear in server log // to point on indicated bad map design TeamBases[0].SendHome(); } if ( TeamBases[1] == none ) SpawnBaseGuardian(1); TeamBases[1].SendHome(); if ( ScrnGameLength != none && NextWave >= ScrnGameLength.Waves.length ) { // if there are not enough waves in ScrnGameLength, then just re-load the last one again WaveNum = ScrnGameLength.Waves.length - 2; } super.DoWaveEnd(); if ( !bGameEnded ) { WaveNum = NextWave; } TSCGRI.WaveNumber = WaveNum; if ( bPendingShuffle ) { ShuffleTeams(); WaveCountDown += 30; TSCGRI.TimeToNextWave = WaveCountDown; } } function SelectShop() { local array TempShopList; local int i, t; // Can't select a shop if there aren't any if ( ShopList.Length == 0 ) return; for ( i = 0; i < ShopList.Length; i++ ) { if ( !ShopList[i].bAlwaysClosed ) TempShopList[TempShopList.Length] = ShopList[i]; } if ( TempShopList.Length == 1 ) { // just to avoid deadends, but TSC games shouldn't be played on maps with only 1 shop TSCGRI.CurrentShop = TempShopList[0]; TSCGRI.BlueShop = TempShopList[0]; return; } // select random shop for random team i = Rand(TempShopList.Length); if ( TempShopList[i] == TSCGRI.CurrentShop ) t = 1; // can't be the same shop twice for the red team, so set it for blue else if ( TempShopList[i] == TSCGRI.BlueShop ) t = 0; // can't be the same shop twice for the blue team, so set it for red else t = rand(2); // set shop for random team TeamShops[t] = TempShopList[i]; TempShopList.remove(i, 1); // select shop for the other team t = 1 - t; i = Rand(TempShopList.Length); if ( TempShopList[i] == TSCGRI.GetTeamShop(t) ) { // can't use the same shop twice TempShopList.remove(i, 1); i = Rand(TempShopList.Length); } TeamShops[t] = TempShopList[i]; // write shops to GRI TSCGRI.CurrentShop = TeamShops[0]; TSCGRI.BlueShop = TeamShops[1]; } function OpenShops() { local int i; local Controller C; bTradingDoorsOpen = True; for( i=0; i0 ) { // Disable pawn collision during trader time C.Pawn.bBlockActors = !bAntiBlocker; if( KFPlayerController(C) !=None ) { KFPlayerController(C).SetShowPathToTrader(true); KFPlayerController(C).ClientLocationalVoiceMessage(C.PlayerReplicationInfo, none, 'TRADER', 2); } } } } // function float GetMinSpawnDelay() // { // local float result; // result = super.GetMinSpawnDelay(); // if ( !bTeamWiped ) // result *= 0.5; // up to twice faster spawns // return result; // } } //MatchInProgress defaultproperties { RedTeamHumanName="British" BlueTeamHumanName="Steampunk" OvertimeWaves=2 SudDeathWaves=1 OvertimeTeamMoneyPenalty=0.330000 LateJoinerCashBonus=700 SongRedWin="KF_WPrevention" SongBlueWin="KFh6_Launch" SongBothWin="KF_Containment" SongBothWiped="KF_Hunger" strBudgetCut="Team wallets were cut off by" BaseRadius=1250.000000 MinBaseZ=-60.000000 MaxBaseZ=200.000000 WaveEndingCountdown=30 BaseGuardianClasses(0)=Class'ScrnBalanceSrv.TSCGuardianRed' BaseGuardianClasses(1)=Class'ScrnBalanceSrv.TSCGuardianBlue' BaseWhisp=Class'ScrnBalanceSrv.GreenWhisp' bLockTeamsOnSuddenDeath=True ClanTags(0)=(Prefix="[",Postfix="]") ClanTags(1)=(Prefix="(",Postfix=")") ClanTags(2)=(Prefix="-=",Postfix="=-") ClanTags(3)=(Prefix="{",Postfix="}") ClanTags(4)=(Prefix="<",Postfix=">") ClanTags(5)=(Prefix=".:",Postfix=":.") ClanTags(6)=(Prefix="*",Postfix="*") ClanTags(7)=(Postfix="-") ClanTags(8)=(Prefix=".") bClanCheck=True HumanDamageMode=HDMG_Normal bVoteHDmg=True HDmgScale=0.100000 bSingleTeamGame=False KFGameLength=1 bUseEndGameBoss=False StandardMaxZombiesOnce=48 KFHints(0)="Each team has own Trader. Team can not get the same Trader two times in a row." KFHints(1)="When the Trader opens her doors, she drops Base Guardian nearby. Take it to your base!" KFHints(2)="Pick up a Base Guardian next to the Trader, bring it where you want your Base to be and press SETUPBASE(ALTFIRE) button." KFHints(3)="Base can be established only once per wave. Once set up, it can not be moved." KFHints(4)="If nobody stays at a Base, then the Guardian gets frustrated and disappears. No Guardian = No Base." KFHints(5)="Base Guardian has 2 cool features: it protects you from the Friendly Fire and damages enemy squad members." KFHints(6)="Base Guardian hurts enemy players withing the range of the Base no matter of Friendly Fire setting." KFHints(7)="Base can be established during the Trader Time only. So hurry up!" KFHints(8)="Other squad cannot stay at your Base." KFHints(9)="You can play without a Base too, but who is going to protect you from the Friendly Fire then?.." KFHints(10)="Setting up a Base in a strategical map point is a key to success." KFHints(11)="You can not set up own Base at Enemy Base. However, Bases may intersect." KFHints(12)="While carrying Base Guardian you can pass it to other player by pressing the same key as throwing a weapon." KFHints(13)="Best camping spots in standard KF game are not necessarily the best spots in TSC." KFHints(14)="Team gets wiped if ANY its member dies during SUDDEN DEATH wave." KFHints(15)="Wiping enemy squad doesn't grant you a win. You still have to survive till the end of the wave." KFHints(16)="TSC isn't Versus or Team Deathmatch game. You don't have to fight the enemy team. Leave that job to ZEDs." KFHints(17)="You can wait until ZEDs wipe out enemy squad or help them. Help who? ZEDs or the other squad? Thr choice is up to you..." KFHints(18)="There are 4 Human Damage rules in TSC: OFF, No Friendly Fire, Normal and PvP. Type MVOTE TEAM HDMG for the info." KFHints(19)="When Human Damage is OFF, you can not damage other squad's members. Your Guardian can though." KFHints(20)="Human Damage is OFF during the first wave, Trader Time and when there are less than 10 zeds left in a wave." KFHints(21)="Human Damage is ON during waves (except first one). Staying at own Base protects you from it." KFHints(22)="You can switch team during the Trader Time. Type SWITCHTEAM in the console or bind it to key." KFHints(23)="Switching the team during the game kills you first. Then restarts you as other squad's member." KFHints(24)="You can not switch a team during a wave." KFHints(25)="Medics and medics only can see health and armor of enemy players" KFHints(26)="TSC, same as KF, is not about winning or losing. It is all about surviving." KFHints(27)="TSC doesn't force you to play against the other squad. You can cooperate and survive together. But nobody said that the other squad thinks the same..." KFHints(28)="During the Trader Time, if other squad has 2 players less than yours, you can switch to it on-the-fly (without dying)." MaxZombiesOnce=48 FinalWave=7 FriendlyFireScale=0.100000 PathWhisps(1)="ScrnBalanceSrv.BlueWhisp" MinNetPlayers=2 DefaultEnemyRosterClass="ScrnBalanceSrv.TSCTeam" MaxPlayers=12 GameReplicationInfoClass=Class'ScrnBalanceSrv.TSCGameReplicationInfo' GameName="Team Survival Competition" Description="Two Teams, One Floor. Killing Floor. There are two teams competing in surviving specimen invasion on the same map and at the same time. Both teams can cooperate, fight against each other or just stay each in own corner of the map - choice is up to you..." ScreenShotName="TSC_T.Team.TSC" }