1550 lines
44 KiB
Ucode
1550 lines
44 KiB
Ucode
//=============================================================================
|
|
// KFGameInfo_VersusSurvival
|
|
//=============================================================================
|
|
// The game mode class for players vs zeds survival game type
|
|
//=============================================================================
|
|
// Killing Floor 2
|
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
|
// - John "Ramm-Jaeger" Gibson
|
|
//=============================================================================
|
|
class KFGameInfo_VersusSurvival extends KFGameInfo_Survival;
|
|
|
|
/** The maximum global number of puke mines that can be active in play */
|
|
const MAX_ACTIVE_PUKE_MINES = 30;
|
|
|
|
/** Anti-griefing system */
|
|
var protected const float ANTI_GRIEF_DELAY;
|
|
var protected const float ANTI_GRIEF_INTERVAL;
|
|
var protected const float ANTI_GRIEF_DAMAGE_PERCENTAGE;
|
|
|
|
/** Zed pawn classes used by players */
|
|
var protected const array<class<KFPawn_Monster> > PlayerZedClasses;
|
|
var protected const array<class<KFPawn_Monster> > PlayerBossClassList;
|
|
|
|
/** Cached reference to versus gamereplicationinfo */
|
|
var KFGameReplicationInfoVersus MyKFGRIV;
|
|
|
|
/** Damagetype used for anti-griefing system */
|
|
var class<KFDamageType> AntiGriefDamageTypeClass;
|
|
|
|
/** Whether auto team balance is enabled */
|
|
var config bool bTeamBalanceEnabled;
|
|
|
|
/** Radius around a damaged zed to award score to player zeds */
|
|
var config float ScoreRadius;
|
|
|
|
/** The delay at the end of a game before auto-switching to the next round */
|
|
var int TimeUntilNextRound;
|
|
|
|
/** The delay after a round is over before opening the round results screen */
|
|
var float RoundEndCinematicDelay;
|
|
|
|
/** Cooldown period after the round results menu is closed but before spawning starts */
|
|
var float PostRoundWaitTime;
|
|
|
|
/** Stat tracking for scoring */
|
|
var protected int WaveBonus;
|
|
var protected int BossDamageDone;
|
|
var protected int BossSurvivorDamageTaken;
|
|
var protected float PercentOfZedsKilledBeforeWipe;
|
|
|
|
/**
|
|
* DLO (by string) content classes specific to this game type
|
|
* - Use when direct ref loading is not possible (e.g. unused GameInfo is loaded)
|
|
* - Class refs can be cached using the supplied GRI
|
|
*/
|
|
static function PreloadGlobalContentClasses()
|
|
{
|
|
local class<KFPawn_Monster> PawnClass;
|
|
|
|
super.PreloadGlobalContentClasses();
|
|
|
|
foreach default.PlayerZedClasses(PawnClass)
|
|
{
|
|
PawnClass.static.PreloadContent();
|
|
}
|
|
}
|
|
|
|
event PreBeginPlay()
|
|
{
|
|
super.PreBeginPlay();
|
|
|
|
// Create the zed team
|
|
CreateTeam(1);
|
|
}
|
|
|
|
function InitGRIVariables()
|
|
{
|
|
super.InitGRIVariables();
|
|
|
|
MyKFGRIV = KFGameReplicationInfoVersus( MyKFGRI );
|
|
MyKFGRIV.bTeamBalanceEnabled = bTeamBalanceEnabled;
|
|
}
|
|
|
|
function bool IsPlayerReady( KFPlayerReplicationInfo PRI )
|
|
{
|
|
// Spawn our player even if we don't have a perk while using the editor or skip lobby
|
|
if (class'KFGameEngine'.static.CheckSkipLobby() || class'Engine'.static.IsEditor())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return super(KFGameInfo).IsPlayerReady(PRI);
|
|
}
|
|
|
|
function StartMatch()
|
|
{
|
|
local KFPlayerController KFPC;
|
|
local array<KFPlayerController> PlayerControllers;
|
|
|
|
// Get a list of players
|
|
foreach WorldInfo.AllControllers(class'KFPlayerController', KFPC)
|
|
{
|
|
PlayerControllers[PlayerControllers.Length] = KFPC;
|
|
if( KFPC.GetTeamNum() == 255 && KFPC.CanRestartPlayer() && (KFPC.Pawn == none || KFPawn_Customization(KFPC.Pawn) != none) )
|
|
{
|
|
if( KFPC.Pawn != none )
|
|
{
|
|
KFPC.Pawn.Destroy();
|
|
}
|
|
|
|
KFPC.StartSpectate();
|
|
}
|
|
}
|
|
|
|
// If there's only one player, we must spawn them in as human
|
|
if( PlayerControllers.Length == 1 )
|
|
{
|
|
SetTeam( PlayerControllers[0], Teams[0] );
|
|
}
|
|
|
|
// Clear round over flag
|
|
MyKFGRIV.bRoundIsOver = false;
|
|
MyKFGRIV.bNetDirty = true;
|
|
MyKFGRIV.bForceNetUpdate = true;
|
|
|
|
super.StartMatch();
|
|
}
|
|
|
|
function bool ShouldStartMatch()
|
|
{
|
|
`if(`notdefined(ShippingPC))
|
|
if( bWaitForNetPlayers && WorldInfo.NetMode != NM_Standalone )
|
|
{
|
|
return NumPlayers >= 1;
|
|
}
|
|
return true;
|
|
`endif
|
|
|
|
return ( WorldInfo.NetMode == NM_StandAlone || (Teams[0].Size > 0 && Teams[1].Size > 0) );
|
|
}
|
|
|
|
function StripAbsentPlayers()
|
|
{
|
|
local int GroupIndex, PlayerIndex;
|
|
local UniqueNetId StupidUnrealBS;
|
|
|
|
`log("StripAbsentPlayers: Stripping players who are no longer logged in from matchmaking groups.", bLogGroupTeamBalance);
|
|
for (GroupIndex = PlayerGroups.length - 1; GroupIndex >= 0; --GroupIndex)
|
|
{
|
|
for (PlayerIndex = PlayerGroups[GroupIndex].PlayerGroup.Length - 1; PlayerIndex >= 0; --PlayerIndex)
|
|
{
|
|
if (GetPRIById(PlayerGroups[GroupIndex].PlayerGroup[PlayerIndex]) == none)
|
|
{
|
|
if (bLogGroupTeamBalance)
|
|
{
|
|
StupidUnrealBS = PlayerGroups[GroupIndex].PlayerGroup[PlayerIndex];
|
|
`log("StripAbsentPlayers: Removing player" @ class'OnlineSubsystem'.static.UniqueNetIdToString(StupidUnrealBS) @ "from group" @ GroupIndex @ "because they are no longer logged in.", bLogGroupTeamBalance );
|
|
}
|
|
PlayerGroups[GroupIndex].PlayerGroup.Remove(PlayerIndex, 1);
|
|
}
|
|
}
|
|
if (PlayerGroups[GroupIndex].PlayerGroup.length == 0)
|
|
{
|
|
`log("StripAbsentPlayers: Removing empty matchmaking group " @ GroupIndex @ "from group list because it is empty.", bLogGroupTeamBalance );
|
|
PlayerGroups.Remove(GroupIndex, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
function SplitArrayByTeam(out PlayerGroupStruct Group, out PlayerGroupStruct Other)
|
|
{
|
|
local int i;
|
|
local PlayerReplicationInfo PRI;
|
|
local UniqueNetId StupidUnrealscriptBS;
|
|
|
|
//Clear out the Other struct
|
|
Other.PlayerGroup.length = 0;
|
|
Other.Team = Group.Team == 1 ? 0 : 1;
|
|
|
|
//The group should have a team by now, but doublecheck and clear it out
|
|
//if it doesn't and quit
|
|
if (Group.Team == 128)
|
|
{
|
|
`log("SplitArrayByTeam: Aborting and removing group because it hasn't yet been assigned a team.");
|
|
Other.Team = 128;
|
|
Group.PlayerGroup.length = 0;
|
|
return;
|
|
}
|
|
//Iterate backwards because that makes the logic of removing an item simpler
|
|
for (i = Group.PlayerGroup.length - 1; i >= 0; --i)
|
|
{
|
|
PRI = GetPRIById(Group.PlayerGroup[i]);
|
|
//Check again that the player in the group is still connected
|
|
if (PRI == none)
|
|
{
|
|
if (bLogGroupTeamBalance)
|
|
{
|
|
StupidUnrealscriptBS = Group.PlayerGroup[i];
|
|
`log("SplitArrayByTeam: Removing player" @ class'OnlineSubsystem'.static.UniqueNetIdToString(StupidUnrealscriptBS) @ "from group because they are not logged in.");
|
|
}
|
|
Group.PlayerGroup.Remove(i, 1);
|
|
continue;
|
|
}
|
|
//If the team they're currently in doesn't match the one they were assigned, put them into
|
|
//the other player group with the other team
|
|
if (PRI.Team != Teams[Group.Team])
|
|
{
|
|
if (bLogGroupTeamBalance)
|
|
{
|
|
StupidUnrealscriptBS = Group.PlayerGroup[i];
|
|
`log("SplitArrayByTeam: Moving player" @ class'OnlineSubsystem'.static.UniqueNetIdToString(StupidUnrealscriptBS) @ "from group in team" @ Group.Team @ "to group in team" @ Other.Team @ "because they chose that team");
|
|
}
|
|
Other.PlayerGroup.AddItem(Group.PlayerGroup[i]);
|
|
Group.PlayerGroup.Remove(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
function SplitGroups()
|
|
{
|
|
//if players have chosen to be in different teams, consider them as singles or separate groups
|
|
//for balancing purposes
|
|
local int i;
|
|
local PlayerGroupStruct NewGroup;
|
|
local PlayerGroupStruct StupidUnrealscriptBS;
|
|
|
|
`log("SplitGroups: Splitting groups with players on separate teams.", bLogGroupTeamBalance);
|
|
|
|
//iterate from end to make adding and removing elements logic simpler
|
|
for (i = PlayerGroups.length - 1; i >= 0; --i)
|
|
{
|
|
StupidUnrealscriptBS = PlayerGroups[i];
|
|
SplitArrayByTeam(StupidUnrealscriptBS, NewGroup);
|
|
PlayerGroups[i] = StupidUnrealscriptBS;
|
|
if (PlayerGroups[i].PlayerGroup.length < 2)
|
|
{
|
|
if (NewGroup.PlayerGroup.length > 1)
|
|
{
|
|
`log("SplitGroups: Changing group number" @ i @ "from team" @ PlayerGroups[i].Team @ "to team" @ NewGroup.Team @ "(now has" @ NewGroup.PlayerGroup.length @ "members).", bLogGroupTeamBalance);
|
|
PlayerGroups[i] = NewGroup;
|
|
}
|
|
else
|
|
{
|
|
`log("SplitGroups: Removing empty group number" @ i, bLogGroupTeamBalance);
|
|
PlayerGroups.Remove(i,1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (NewGroup.PlayerGroup.length > 0)
|
|
{
|
|
`log("SplitGroups: Adding a new group of team" @ NewGroup.Team @ "at position" @ i+1 @ "with" @ NewGroup.PlayerGroup.length @ "members.", bLogGroupTeamBalance);
|
|
PlayerGroups.Insert(i+1, 1);
|
|
PlayerGroups[i+1] = NewGroup;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Returns human team size - zed team size
|
|
function int GetDelta()
|
|
{
|
|
return Teams[0].Size - Teams[1].Size;
|
|
}
|
|
|
|
//Returns true if the game is playable (neither team over 6) by only moving players not in a group
|
|
//AdditionalDelta is number moved from Zed to Human team, negative for the other direction
|
|
function bool IsBalanceable(int NumHumanSingles, int NumZedSingles, optional int AdditionalDelta = 0)
|
|
{
|
|
if (Teams[0].Size > 0 && Teams[0].Size <= `KF_MAX_PLAYERS && Teams[1].Size <= `KF_MAX_PLAYERS)
|
|
{
|
|
return true;
|
|
}
|
|
if (Teams[0].Size - NumHumanSingles + AdditionalDelta > `KF_MAX_PLAYERS || Teams[1].Size - NumZedSingles - AdditionalDelta > `KF_MAX_PLAYERS)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//Returns true if it's possible to play a Versus match with the current team balance
|
|
//Does not mean balance is optimal (Delta within
|
|
function bool IsLegal()
|
|
{
|
|
return Teams[0].Size > 0 && Teams[0].Size <= `KF_MAX_PLAYERS && Teams[1].Size <= `KF_MAX_PLAYERS;
|
|
}
|
|
|
|
//Moves single players from team to team until they are equal or within one of each other
|
|
//If bTryAnyway is false, will do nothing if they can't be balanced within 1
|
|
//If bTryAnyway is true, will get as close as possible
|
|
//Returns true if any players were moved or the teams already within 1
|
|
function bool BalanceSingles(out array<PlayerReplicationInfo> HumanSingles, out array<PlayerReplicationInfo> ZedSingles, optional bool bTryAnyway = false)
|
|
{
|
|
local int Delta;
|
|
local bool rc;
|
|
|
|
|
|
if (!IsBalanceable(HumanSingles.length, ZedSingles.length) && !bTryAnyway)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Delta = GetDelta();
|
|
|
|
if (abs(Delta) <= 1)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
rc = false;
|
|
if (Delta < 0)
|
|
{
|
|
while (ZedSingles.length > 0 && Delta < 0)
|
|
{
|
|
SwapTeamFor(ZedSingles[ZedSingles.length-1]);
|
|
ZedSingles.Remove(ZedSingles.length-1, 1);
|
|
Delta = GetDelta();
|
|
rc = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (HumanSingles.length > 0 && Delta > 0)
|
|
{
|
|
SwapTeamFor(HumanSingles[HumanSingles.length-1]);
|
|
HumanSingles.Remove(HumanSingles.length-1, 1);
|
|
Delta = GetDelta();
|
|
rc = true;
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
function BalanceTeams()
|
|
{
|
|
local int Delta;
|
|
local PlayerReplicationInfo PRI;
|
|
local array<PlayerReplicationInfo> HumanSingles, ZedSingles;
|
|
|
|
StripAbsentPlayers();
|
|
SplitGroups();
|
|
|
|
Delta = Teams[0].Size - Teams[1].Size;
|
|
if ( Delta == 0 )
|
|
return;
|
|
|
|
foreach MyKFGRIV.PRIArray(PRI)
|
|
{
|
|
if (GetPRIById(PRI.UniqueId) == none)
|
|
{
|
|
if (PRI.Team == Teams[0])
|
|
{
|
|
HumanSingles.AddItem(PRI);
|
|
}
|
|
else
|
|
{
|
|
ZedSingles.AddItem(PRI);
|
|
}
|
|
}
|
|
}
|
|
if (BalanceSingles(HumanSingles, ZedSingles))
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
BalanceTeamsOld();
|
|
}
|
|
}
|
|
|
|
function BalanceTeamsOld()
|
|
{
|
|
local int Delta, AutoBalanceRemaining, i;
|
|
local TeamInfo TI;
|
|
local Array<PlayerReplicationInfo> AutoBalanceList;
|
|
local PlayerReplicationInfo PRI;
|
|
|
|
Delta = Teams[1].Size - Teams[0].Size;
|
|
if ( Delta == 0 )
|
|
return;
|
|
|
|
TI = (Delta > 0) ? Teams[1] : Teams[0];
|
|
for (i = 0; i < MyKFGRIV.PRIArray.Length; i++)
|
|
{
|
|
PRI = MyKFGRIV.PRIArray[i];
|
|
if ( PRI.Team == TI )
|
|
{
|
|
AutoBalanceList.AddItem(PRI);
|
|
}
|
|
}
|
|
|
|
AutoBalanceRemaining = Min(abs(Delta), MyKFGRIV.TeamBalanceDelta);
|
|
while (AutoBalanceRemaining > 0 || TI.Size > (MaxPlayersAllowed / 2) )
|
|
{
|
|
i = Rand(AutoBalanceList.Length);
|
|
SwapTeamFor(AutoBalanceList[i]);
|
|
AutoBalanceList.Remove(i, 1);
|
|
AutoBalanceRemaining--;
|
|
}
|
|
}
|
|
|
|
function SwapTeamFor( PlayerReplicationInfo PRI )
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
|
|
KFPCV = KFPlayerControllerVersus(PRI.Owner);
|
|
if( KFPCV != none )
|
|
{
|
|
KFPCV.NotifyOfAutoBalance();
|
|
SetTeam( KFPCV, PRI.GetTeamNum() == 255 ? Teams[0] : Teams[1] );
|
|
}
|
|
}
|
|
|
|
/* create a player team, and fill from the team roster
|
|
*/
|
|
function CreateTeam(int TeamIndex)
|
|
{
|
|
switch (TeamIndex)
|
|
{
|
|
case 0:
|
|
Teams[TeamIndex] = spawn(class'KFGame.KFTeamInfo_Human');
|
|
GameReplicationInfo.SetTeam(TeamIndex, Teams[TeamIndex]);
|
|
break;
|
|
|
|
case 1:
|
|
Teams[TeamIndex] = spawn(class'KFGameContent.KFTeamInfo_Zeds');
|
|
GameReplicationInfo.SetTeam(TeamIndex, Teams[TeamIndex]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function int GetPlayerGroup(const out UniqueNetId PlayerId, optional out int IdIndex)
|
|
{
|
|
local PlayerGroupStruct Group;
|
|
local UniqueNetId Id;
|
|
local int GroupIndex, Index;
|
|
|
|
IdIndex = INDEX_NONE;
|
|
foreach PlayerGroups(Group, GroupIndex)
|
|
{
|
|
foreach Group.PlayerGroup(Id, Index)
|
|
{
|
|
if (id == PlayerId)
|
|
{
|
|
IdIndex = Index;
|
|
return GroupIndex;
|
|
}
|
|
}
|
|
}
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
function byte PickGroupTeam(optional int GroupSize = 1)
|
|
{
|
|
local int Human, Zed;
|
|
|
|
GetReservedTotals(Human, Zed);
|
|
//Keep the human team ahead of Zeds, since there will always be AI Zeds to fill in
|
|
if (Zed + GroupSize <= Human)
|
|
{
|
|
`log("PickGroupTeam: Team totals are Humans" @ Human @ "Zeds" @ Zed @ ", adding new group to Zed team", bLogGroupTeamBalance);
|
|
//Zed team
|
|
return 1;
|
|
}
|
|
else
|
|
{
|
|
`log("PickGroupTeam: Team totals are Humans" @ Human @ "Zeds" @ Zed @ ", adding new group to Human team", bLogGroupTeamBalance);
|
|
//Human team
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function PlayerReplicationInfo GetPRIById(const UniqueNetId Id)
|
|
{
|
|
local PlayerReplicationInfo PRI;
|
|
|
|
foreach MyKFGRIV.PRIArray(PRI)
|
|
{
|
|
if (PRI.UniqueId == Id)
|
|
{
|
|
return PRI;
|
|
}
|
|
}
|
|
return none;
|
|
}
|
|
|
|
/* Adds up the players on each team, including those who are in a group
|
|
assigned to a team but not yet logged in */
|
|
function GetReservedTotals(out int Human, out int Zed)
|
|
{
|
|
local PlayerGroupStruct Group;
|
|
local UniqueNetId Id;
|
|
|
|
//First total who is logged in
|
|
Human = Teams[0].Size;
|
|
Zed = Teams[1].Size;
|
|
|
|
foreach PlayerGroups(Group)
|
|
{
|
|
foreach Group.PlayerGroup(Id)
|
|
{
|
|
//If they they have a PRI they have already logged in and were counted in the
|
|
//Team[].Size values above
|
|
if (GetPRIById(Id) == none)
|
|
{
|
|
//If they haven't logged in but their group has been assigned a team, count them
|
|
//in that team. If Team is something other than 0 or 1 they haven't been assigned a
|
|
//team and so don't count them yet
|
|
if (Group.Team == 0)
|
|
{
|
|
++Human;
|
|
}
|
|
else if (Group.Team == 1)
|
|
{
|
|
++Zed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Called by Gameinfo::Login(), initial team pick */
|
|
function byte PickTeam(byte Current, Controller C, const out UniqueNetId PlayerId)
|
|
{
|
|
local int Group;
|
|
`if(`notdefined(ShippingPC))
|
|
// Allow us to force join as the zed team if we pass 128 as our team ID to the command line
|
|
if( Current == 128 )
|
|
{
|
|
`log( "[DEBUGVERSUS] Force joining zed team using ?Team=128 from command line!" );
|
|
return 1;
|
|
}
|
|
`endif
|
|
Group = GetPlayerGroup(PlayerId);
|
|
if (Group == INDEX_NONE)
|
|
{
|
|
//If they're not in a group, consider them as a group of one
|
|
return PickGroupTeam();
|
|
}
|
|
else if (PlayerGroups[Group].Team != 0 && PlayerGroups[Group].Team != 1)
|
|
{
|
|
//If their group hasn't been assigned a team yet (should only happen for the first member
|
|
//who connects), pick a team for them based on their group size
|
|
PlayerGroups[Group].Team = PickGroupTeam(PlayerGroups[Group].PlayerGroup.length);
|
|
}
|
|
//If we make it here, they should have a team, so return it
|
|
return PlayerGroups[Group].Team;
|
|
}
|
|
|
|
/** Return whether a team change is allowed. */
|
|
function bool ChangeTeam(Controller Other, int N, bool bNewTeam)
|
|
{
|
|
if (ArrayCount(Teams) > N)
|
|
{
|
|
if( Other.PlayerReplicationInfo == none
|
|
|| Other.PlayerReplicationInfo.bBot
|
|
|| ( !Other.PlayerReplicationInfo.bOnlySpectator
|
|
&& Other.PlayerReplicationInfo.Team != Teams[N] ) )
|
|
{
|
|
SetTeam( Other, Teams[N] );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** SetTeam()
|
|
* Change Other's team to NewTeam.
|
|
* @param Other: the controller which wants to change teams
|
|
* @param NewTeam: the desired team.
|
|
* @param bNewTeam: if true, broadcast team change notification
|
|
*/
|
|
function SetTeam(Controller Other, KFTeamInfo_Human NewTeam)
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
local TeamInfo OldTeam;
|
|
|
|
if( Other == none || Other.PlayerReplicationInfo == none || Other.PlayerReplicationInfo.bBot || Other.PlayerReplicationInfo.bOnlySpectator )
|
|
{
|
|
super.SetTeam( Other, NewTeam );
|
|
return;
|
|
}
|
|
|
|
OldTeam = Other.PlayerReplicationInfo.Team;
|
|
|
|
super.SetTeam( Other, NewTeam );
|
|
|
|
if( OldTeam != none && NewTeam != OldTeam )
|
|
{
|
|
KFPCV = KFPlayerControllerVersus( Other );
|
|
KFPCV.ServerNotifyTeamChanged();
|
|
if( !IsPlayerReady(KFPlayerReplicationInfo(Other.PlayerReplicationInfo)) )
|
|
{
|
|
OnWaitingPlayerTeamSwapped( Other );
|
|
}
|
|
else
|
|
{
|
|
if( OldTeam != none && OldTeam.TeamIndex == 255 && KFPCV != none && KFPCV.PlayerZedSpawnInfo.PendingZedPawnClass != none )
|
|
{
|
|
// If this player switched teams, we need to recycle their zed pawn class back into rotation
|
|
if( SpawnManager != none )
|
|
{
|
|
KFAISpawnManager_Versus(SpawnManager).RecyclePendingZedPawnClass( KFPCV );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Overridden to handle team changes right after login */
|
|
event PostLogin( PlayerController NewPlayer )
|
|
{
|
|
super.PostLogin( NewPlayer );
|
|
|
|
if( !NewPlayer.CanRestartPlayer() )
|
|
{
|
|
OnWaitingPlayerTeamSwapped( NewPlayer );
|
|
}
|
|
}
|
|
|
|
/** Initializes player state on team swap */
|
|
function OnWaitingPlayerTeamSwapped( Controller C )
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
local NavigationPoint Start;
|
|
|
|
KFPCV = KFPlayerControllerVersus( C );
|
|
if( KFPCV != none )
|
|
{
|
|
if( KFPCV.GetTeamNum() == 255 )
|
|
{
|
|
if( KFPCV.Pawn != none && KFPawn_Customization(KFPCV.Pawn) != none )
|
|
{
|
|
// Hide customization pawn
|
|
KFPawn_Customization(KFPCV.Pawn).SetServerHidden( true );
|
|
}
|
|
|
|
KFPCV.SetCameraMode( 'PlayerZedWaiting' );
|
|
}
|
|
else if( KFPCV.Pawn != none )
|
|
{
|
|
// Unhide/relocate customization pawn
|
|
KFPawn_Customization(KFPCV.Pawn).SetServerHidden( false );
|
|
|
|
if( !KFPawn_Customization(KFPCV.Pawn).MoveToCustomizationPoint() )
|
|
{
|
|
Start = KFPCV.GetBestCustomizationStart( self );
|
|
if( Start != none )
|
|
{
|
|
KFPawn_Customization(KFPCV.Pawn).SetUpdatedMovementData( Start.Location, Start.Rotation );
|
|
}
|
|
}
|
|
|
|
// Restore view target and camera mode
|
|
KFPCV.SetViewTarget( KFPCV.Pawn );
|
|
KFPCV.SetCameraMode( 'Customization' );
|
|
}
|
|
|
|
KFPCV.ServerNotifyTeamChanged();
|
|
}
|
|
}
|
|
|
|
/** Need to recycle any exiting players' pending zed pawn classes back into the spawn rotation */
|
|
function Logout( Controller Exiting )
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
|
|
if( Exiting != none )
|
|
{
|
|
KFPCV = KFPlayerControllerVersus( Exiting );
|
|
if( KFPCV != none && KFPCV.GetTeamNum() == 255 && KFPCV.PlayerZedSpawnInfo.PendingZedPawnClass != none )
|
|
{
|
|
if( SpawnManager != none )
|
|
{
|
|
KFAISpawnManager_Versus(SpawnManager).RecyclePendingZedPawnClass( KFPCV );
|
|
}
|
|
}
|
|
}
|
|
|
|
super.Logout( Exiting );
|
|
}
|
|
|
|
function RestartPlayer(Controller NewPlayer)
|
|
{
|
|
local int PlayerTeamIndex;
|
|
local KFPawn_Monster MonsterPawn;
|
|
local KFPlayerController KFPC;
|
|
|
|
// Check bOnlySpectator. Can be called sometimes without calling CanRestartPlayer()?!?
|
|
if(NewPlayer.PlayerReplicationInfo == none || NewPlayer.PlayerReplicationInfo.bOnlySpectator)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PlayerTeamIndex = NewPlayer.GetTeamNum();
|
|
KFPC = KFPlayerController( NewPlayer );
|
|
if( KFPC != none
|
|
&& (MyKFGRI.bMatchIsOver || (PlayerTeamIndex == 255 && (MyKFGRI.bTraderIsOpen || KFPC.PlayerZedSpawnInfo.PendingZedPawnClass == none))) )
|
|
{
|
|
if( IsPlayerReady(KFPlayerReplicationInfo(KFPC.PlayerReplicationInfo)) )
|
|
{
|
|
KFPC.StartSpectate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if( NewPlayer.Pawn != none && KFPawn_Customization(NewPlayer.Pawn) != none )
|
|
{
|
|
// If we have a customization pawn, destroy it before spawning a new pawn with super.RestartPlayer
|
|
NewPlayer.Pawn.Destroy();
|
|
}
|
|
|
|
// Restart human
|
|
if( PlayerTeamIndex != 255 )
|
|
{
|
|
super.RestartPlayer( NewPlayer );
|
|
}
|
|
else if( NewPlayer.Pawn == none && PlayerTeamIndex == 255 )
|
|
{
|
|
super(GameInfo).RestartPlayer( NewPlayer );
|
|
}
|
|
|
|
MonsterPawn = KFPawn_Monster(NewPlayer.Pawn);
|
|
if( MonsterPawn != none && MonsterPawn.IsHumanControlled() )
|
|
{
|
|
MonsterPawn.UpdateLastTimeDamageHappened();
|
|
}
|
|
}
|
|
|
|
/* @see GameInfo::SetPlayerDefaults */
|
|
function SetPlayerDefaults(Pawn PlayerPawn)
|
|
{
|
|
local KFPawn_Human P;
|
|
|
|
Super.SetPlayerDefaults(PlayerPawn);
|
|
|
|
// humans get full armor for the first wave
|
|
if ( WaveNum == 0 )
|
|
{
|
|
P = KFPawn_Human(PlayerPawn);
|
|
if ( P != none )
|
|
{
|
|
P.GiveMaxArmor();
|
|
}
|
|
}
|
|
}
|
|
|
|
function int GetAIControlledMonsterAliveCount()
|
|
{
|
|
local AIController AIP;
|
|
local int UsedLivingAIMonsterCount;
|
|
|
|
foreach WorldInfo.AllControllers(class'AIController', AIP)
|
|
{
|
|
if( AIP != none && AIP.Pawn != none && AIP.Pawn.IsAliveAndWell() )
|
|
{
|
|
if( KFPawn_Monster(AIP.Pawn) != none )
|
|
{
|
|
UsedLivingAIMonsterCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return UsedLivingAIMonsterCount;
|
|
}
|
|
|
|
/**
|
|
* Returns the default pawn class for the specified controller,
|
|
*
|
|
* @param C - controller to figure out pawn class for
|
|
*
|
|
* @return default pawn class
|
|
*/
|
|
function class<Pawn> GetDefaultPlayerClass( Controller C )
|
|
{
|
|
local KFPlayerController KFPC;
|
|
|
|
KFPC = KFPlayerController(C);
|
|
|
|
if( KFPC.GetTeamNum() == 255 )
|
|
{
|
|
if( KFPC.PlayerZedSpawnInfo.PendingZedPawnClass != none )
|
|
{
|
|
return KFPC.PlayerZedSpawnInfo.PendingZedPawnClass;
|
|
}
|
|
|
|
return none;
|
|
}
|
|
|
|
// default to the game specified pawn class
|
|
return super.GetDefaultPlayerClass( C );
|
|
}
|
|
|
|
// Overridden because the native implementation will count human controlled zeds
|
|
function int GetLivingPlayerCount()
|
|
{
|
|
local Controller P;
|
|
local int UsedLivingHumanPlayersCount;
|
|
|
|
`if(`notdefined(ShippingPC))
|
|
if ( ForcedNumLivingPlayers != 0 )
|
|
{
|
|
return ForcedNumLivingPlayers;
|
|
}
|
|
`endif
|
|
|
|
foreach WorldInfo.AllControllers(class'Controller', P)
|
|
{
|
|
if( P != none && P.Pawn != none && P.Pawn.IsAliveAndWell() )
|
|
{
|
|
if( P.GetTeamNum() != 255 )
|
|
{
|
|
//`log(P$" TeamIndex = "$P.PlayerReplicationInfo.Team.TeamIndex);
|
|
UsedLivingHumanPlayersCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
//`log(GetFuncName()$" Player alive count: "$UsedLivingHumanPlayersCount);
|
|
|
|
return UsedLivingHumanPlayersCount;
|
|
}
|
|
|
|
/* Use reduce damage for friendly fire, etc. */
|
|
function ReduceDamage( out int Damage, Pawn Injured, Controller InstigatedBy, vector HitLocation, out vector Momentum, class<DamageType> DamageType, Actor DamageCauser, TraceHitInfo HitInfo )
|
|
{
|
|
local KFPawn InstigatorPawn, InjuredPawn;
|
|
|
|
if( InstigatedBy != none )
|
|
{
|
|
InstigatorPawn = KFPawn(InstigatedBy.Pawn);
|
|
InjuredPawn = KFPawn(Injured);
|
|
|
|
if( DamageType != AntiGriefDamageTypeClass )
|
|
{
|
|
if( InstigatorPawn != none && InjuredPawn != none &&
|
|
InstigatorPawn.GetTeamNum() != InjuredPawn.GetTeamNum() )
|
|
{
|
|
InstigatorPawn.UpdateLastTimeDamageHappened();
|
|
InjuredPawn.UpdateLastTimeDamageHappened();
|
|
}
|
|
}
|
|
|
|
// Don't let player zeds damage AI zeds
|
|
if( InstigatedBy.GetTeamNum() == 255
|
|
&& Injured.GetTeamNum() == 255
|
|
&& InstigatedBy.bIsPlayer
|
|
&& !Injured.IsHumanControlled() )
|
|
{
|
|
Momentum = vect(0,0,0);
|
|
Damage = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
super.ReduceDamage( Damage, Injured, InstigatedBy, HitLocation, Momentum, DamageType, DamageCauser, HitInfo );
|
|
}
|
|
|
|
function ScoreDamage( int DamageAmount, int HealthBeforeDamage, Controller InstigatedBy, Pawn DamagedPawn, class<DamageType> damageType )
|
|
{
|
|
local KFInterface_MonsterBoss BossRef;
|
|
|
|
if( InstigatedBy == none
|
|
|| !InstigatedBy.bIsPlayer
|
|
|| InstigatedBy.PlayerReplicationInfo == none
|
|
|| InstigatedBy.GetTeamNum() == DamagedPawn.GetTeamNum() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
DamageAmount = Min( DamageAmount, HealthBeforeDamage );
|
|
KFPlayerReplicationInfo(InstigatedBy.PlayerReplicationInfo).DamageDealtOnTeam += DamageAmount;
|
|
KFPlayerController(InstigatedBy).AddTrackedDamage(DamageAmount, damageType, InstigatedBy.Pawn.Class, DamagedPawn.Class);
|
|
|
|
if(InstigatedBy.PlayerReplicationInfo.GetTeamNum() == 255)
|
|
{
|
|
BossRef = KFInterface_MonsterBoss(InstigatedBy.Pawn);
|
|
if(BossRef != none)
|
|
{
|
|
if(DamagedPawn.IsA('KFPawn_Human'))
|
|
{
|
|
//damage taken from boss
|
|
BossSurvivorDamageTaken += DamageAmount;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
BossRef = KFInterface_MonsterBoss(DamagedPawn);
|
|
if(BossRef != none)
|
|
{
|
|
//damage done to boss
|
|
BossDamageDone += DamageAmount;
|
|
}
|
|
}
|
|
}
|
|
|
|
function ScoreKill(Controller Killer, Controller Other)
|
|
{
|
|
local KFPawn KFP;
|
|
local KFPlayerReplicationInfo DamagerKFPRI;
|
|
local int i;
|
|
|
|
// Add player zed assists
|
|
if( Other.GetTeamNum() == 0 )
|
|
{
|
|
KFP = KFPawn( Other.Pawn );
|
|
for( i = 0; i < KFP.DamageHistory.Length; ++i )
|
|
{
|
|
if( KFP.DamageHistory[i].DamagerController != none
|
|
&& KFP.DamageHistory[i].DamagerController.bIsPlayer
|
|
&& KFP.DamageHistory[i].DamagerPRI != none
|
|
&& KFP.DamageHistory[i].DamagerPRI.GetTeamNum() == 255 )
|
|
{
|
|
if( Killer.PlayerReplicationInfo != KFP.DamageHistory[i].DamagerPRI )
|
|
{
|
|
DamagerKFPRI = KFPlayerReplicationInfo( KFP.DamageHistory[i].DamagerPRI );
|
|
if( DamagerKFPRI != none )
|
|
{
|
|
++DamagerKFPRI.Assists;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if( WaveNum == WaveMax && KFInterface_MonsterBoss(Other.Pawn) != none )
|
|
{
|
|
// Add our boss kill points
|
|
BossDamageDone = POINTS_FOR_BOSS_KILL;
|
|
}
|
|
|
|
super.ScoreKill( Killer, Other );
|
|
}
|
|
|
|
//override from survival to send correct message
|
|
function EndOfMatch(bool bVictory)
|
|
{
|
|
local KFPlayerController KFPC;
|
|
local int TempScore;
|
|
local KFPlayerReplicationInfoVersus KFPRIV;
|
|
|
|
`AnalyticsLog(("match_end", None, "#"$WaveNum, "#"$(bVictory ? "1" : "0"), "#"$GameConductor.ZedVisibleAverageLifespan));
|
|
|
|
if(bVictory)
|
|
{
|
|
SetTimer(EndCinematicDelay, false, nameof(SetWonGameCamera));
|
|
BroadcastLocalizedMessage(class'KFLocalMessage_Priority', GMT_HumansWin);
|
|
}
|
|
else
|
|
{
|
|
BroadcastLocalizedMessage(class'KFLocalMessage_Priority', GMT_ZedsWin);
|
|
SetZedsToVictoryState();
|
|
}
|
|
|
|
foreach WorldInfo.AllControllers(class'KFPlayerController', KFPC)
|
|
{
|
|
if( bVictory )
|
|
{
|
|
KFPC.ClientWonGame( WorldInfo.GetMapName( true ), GameDifficulty, GameLength, IsMultiplayerGame() );
|
|
}
|
|
|
|
KFPRIV = KFPlayerReplicationInfoVersus(KFPC.PlayerReplicationInfo);
|
|
if(KFPRIV != none)
|
|
{
|
|
KFPRIV.RecordEndGameInfo();
|
|
}
|
|
|
|
KFPC.ClientGameOver(WorldInfo.GetMapName(true), GameDifficulty, GameLength, IsMultiplayerGame(), WaveNum);
|
|
}
|
|
|
|
WorldInfo.TWPushLogs();
|
|
|
|
// Calculate final score
|
|
WaveBonus = Max( (WaveNum - 1), 0) * POINTS_FOR_WAVE_COMPLETION;
|
|
|
|
if( bVictory )
|
|
{
|
|
CheckRoundEndAchievements( 0 );
|
|
TempScore += POINTS_FOR_BOSS_KILL;
|
|
TempScore -= BossSurvivorDamageTaken;
|
|
}
|
|
else
|
|
{
|
|
CheckRoundEndAchievements( 255 );
|
|
|
|
if( WaveNum != WaveMax )
|
|
{
|
|
WaveBonus += Max( int(float(POINTS_FOR_WAVE_COMPLETION) * PercentOfZedsKilledBeforeWipe), 0 );
|
|
}
|
|
}
|
|
|
|
TempScore += WaveBonus;
|
|
TempScore -= POINTS_PENALTY_FOR_DEATH * HumanDeaths;
|
|
Teams[0].AddRoundScore( TempScore, true );
|
|
|
|
// Update team scores
|
|
if( MyKFGRIV.CurrentRound == 0 )
|
|
{
|
|
UpdateFirstRoundTeamScore();
|
|
}
|
|
else
|
|
{
|
|
UpdateSecondRoundTeamScore();
|
|
}
|
|
|
|
GotoState('RoundEnded', 'Begin');
|
|
}
|
|
|
|
function Killed( Controller Killer, Controller KilledPlayer, Pawn KilledPawn, class<DamageType> damageType )
|
|
{
|
|
super.Killed( Killer, KilledPlayer, KilledPawn, damageType );
|
|
|
|
if( IsWaveActive() && GetAIControlledMonsterAliveCount() <= 0 && SpawnManager.IsFinishedSpawning() )
|
|
{
|
|
CheckPawnsForGriefing( true );
|
|
}
|
|
}
|
|
|
|
function WaveEnded( EWaveEndCondition WinCondition )
|
|
{
|
|
local KFPlayerReplicationInfoVersus KFPRIV;
|
|
local int WaveKills;
|
|
local int i, j;
|
|
|
|
MyKFGRIV.SetPlayerZedSpawnTime( 255, false );
|
|
ClearTimer( nameOf(CheckPawnsForGriefing) );
|
|
|
|
// If game ended on a wipe, record how many zeds were killed as well as wave reached
|
|
if( WinCondition == WEC_TeamWipedOut && SpawnManager != none && WaveNum != WaveMax )
|
|
{
|
|
PercentOfZedsKilledBeforeWipe = fClamp( 1.f - (float(MyKFGRI.AIRemaining) / float(SpawnManager.WaveTotalAI)), 0.f, 1.f );
|
|
}
|
|
|
|
// Tabulate the number of kills this wave
|
|
for( i = 0; i < WorldInfo.GRI.PRIArray.Length; ++i )
|
|
{
|
|
// No kills to tabulate
|
|
if( WorldInfo.GRI.PRIArray[i].Kills == 0 )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
KFPRIV = KFPlayerReplicationInfoVersus( WorldInfo.GRI.PRIArray[i] );
|
|
if( KFPRIV != none && !KFPRIV.bBot && KFPRIV.GetTeamNum() == 255 )
|
|
{
|
|
WaveKills = KFPRIV.Kills;
|
|
for( j = 0; j < KFPRIV.WaveKills.Length; ++j )
|
|
{
|
|
WaveKills -= KFPRIV.WaveKills[j];
|
|
}
|
|
|
|
KFPRIV.WaveKills[WaveNum] = WaveKills;
|
|
}
|
|
}
|
|
|
|
super.WaveEnded( WinCondition );
|
|
}
|
|
|
|
|
|
function BossDied(Controller Killer, optional bool bCheckWaveEnded = true)
|
|
{
|
|
super.BossDied(Killer, bCheckWaveEnded);
|
|
}
|
|
|
|
/**
|
|
* @brief Checks if a player zed has dealt or received damage
|
|
*
|
|
* @param bool bInitial Set's the LastTimeDamageHappened to the current time to
|
|
* give the players some time to get to the humans
|
|
*/
|
|
protected function CheckPawnsForGriefing( optional bool bInitial=false )
|
|
{
|
|
local Pawn TestPawn;
|
|
local KFPawn_Monster MonsterTestPawn;
|
|
|
|
foreach WorldInfo.AllPawns( class'Pawn', TestPawn )
|
|
{
|
|
if( TestPawn.IsAliveAndWell() && TestPawn.Controller != none &&
|
|
TestPawn.Controller.bIsPlayer && TestPawn.GetTeamNum() == 255 )
|
|
{
|
|
MonsterTestPawn = KFPawn_Monster(TestPawn);
|
|
if( MonsterTestPawn != none )
|
|
{
|
|
if( bInitial )
|
|
{
|
|
MonsterTestPawn.LastTimeDamageHappened = WorldInfo.TimeSeconds;
|
|
}
|
|
else if( WorldInfo.TimeSeconds - MonsterTestPawn.LastTimeDamageHappened >= ANTI_GRIEF_DELAY &&
|
|
MonsterTestPawn.LastTimeDamageHappened != 0 )
|
|
{
|
|
MonsterTestPawn.MotivatePlayerToAttack( ANTI_GRIEF_DAMAGE_PERCENTAGE, AntiGriefDamageTypeClass );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if( IsWaveActive() )
|
|
{
|
|
SetTimer( ANTI_GRIEF_INTERVAL, false, nameOf(CheckPawnsForGriefing) );
|
|
}
|
|
}
|
|
|
|
/** Called to reset all the types of pickups */
|
|
function ResetAllPickups()
|
|
{
|
|
if ( !bDisablePickups )
|
|
{
|
|
// Reset ALL pickups each wave (ignoring NumPickups)
|
|
// -1, so that we always have a different pickup to activate
|
|
NumWeaponPickups = Max(ItemPickups.Length - 1, 0);
|
|
NumAmmoPickups = Max(AmmoPickups.Length - 1, 0);
|
|
}
|
|
|
|
Super.ResetAllPickups();
|
|
}
|
|
|
|
function OpenPostRoundMenu()
|
|
{
|
|
local KFPlayerController KFPC;
|
|
|
|
foreach WorldInfo.AllControllers(class'KFPlayerController', KFPC)
|
|
{
|
|
if( IsPlayerReady(KFPlayerReplicationInfo(KFPC.PlayerReplicationInfo)) )
|
|
{
|
|
KFPC.ClientOpenRoundSummary();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Updates team data for use in the next game round */
|
|
function UpdateFirstRoundTeamScore()
|
|
{
|
|
// Cache team score
|
|
Teams[1].TeamScoreDataPacket.RoundScore = Teams[0].TeamScoreDataPacket.RoundScore;
|
|
// Cache wave reached
|
|
Teams[1].TeamScoreDataPacket.WaveBonus = WaveBonus;
|
|
|
|
// Cache survivor deaths
|
|
Teams[1].TeamScoreDataPacket.Deaths = HumanDeaths;
|
|
|
|
// Cache boss values if relevant
|
|
if( WaveNum == WaveMax )
|
|
{
|
|
// Boss damage done
|
|
Teams[1].TeamScoreDataPacket.BossDamageDone = BossDamageDone;
|
|
|
|
// Boss survivor damage taken
|
|
Teams[1].TeamScoreDataPacket.BossDamageTaken = BossSurvivorDamageTaken;
|
|
}
|
|
else
|
|
{
|
|
// Clear any boss values
|
|
Teams[1].TeamScoreDataPacket.BossDamageDone = 0;
|
|
Teams[1].TeamScoreDataPacket.BossDamageTaken = 0;
|
|
}
|
|
|
|
|
|
// Reset merc team stats for second round
|
|
Teams[0].TeamScoreDataPacket.RoundScore = 0;
|
|
Teams[0].TeamScoreDataPacket.WaveBonus = -1;
|
|
Teams[0].TeamScoreDataPacket.Deaths = 0;
|
|
Teams[0].TeamScoreDataPacket.BossDamageDone = 0;
|
|
Teams[0].TeamScoreDataPacket.BossDamageTaken = 0;
|
|
|
|
// Force network update
|
|
Teams[0].bForceNetUpdate = true;
|
|
Teams[0].bNetDirty = true;
|
|
Teams[1].bForceNetUpdate = true;
|
|
Teams[1].bNetDirty = true;
|
|
}
|
|
|
|
/** Updates team data for use in the final game round */
|
|
function UpdateSecondRoundTeamScore()
|
|
{
|
|
// Cache wave reached
|
|
Teams[0].TeamScoreDataPacket.WaveBonus = WaveBonus;
|
|
|
|
// Cache survivor deaths
|
|
Teams[0].TeamScoreDataPacket.Deaths = HumanDeaths;
|
|
|
|
// Cache boss values if relevant
|
|
if( WaveNum == WaveMax )
|
|
{
|
|
// Boss damage done
|
|
Teams[0].TeamScoreDataPacket.BossDamageDone = BossDamageDone;
|
|
|
|
// Boss survivor damage taken
|
|
Teams[0].TeamScoreDataPacket.BossDamageTaken = BossSurvivorDamageTaken;
|
|
}
|
|
else
|
|
{
|
|
// Clear any boss values
|
|
Teams[0].TeamScoreDataPacket.BossDamageDone = 0;
|
|
Teams[0].TeamScoreDataPacket.BossDamageTaken = 0;
|
|
}
|
|
|
|
// Force network update
|
|
Teams[0].bNetDirty = true;
|
|
Teams[0].bForceNetUpdate = true;
|
|
Teams[1].bNetDirty = true;
|
|
Teams[1].bForceNetUpdate = true;
|
|
}
|
|
|
|
/** Perform gametype-specific resets */
|
|
function Reset()
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
|
|
super.Reset();
|
|
|
|
// Swap teams
|
|
foreach WorldInfo.AllControllers( class'KFPlayerControllerVersus', KFPCV )
|
|
{
|
|
if( KFPCV.CanRestartPlayer() )
|
|
{
|
|
SetTeam( KFPCV, KFPCV.PlayerReplicationInfo.Team == Teams[0] ? Teams[1] : Teams[0] );
|
|
KFPCV.ServerNotifyTeamChanged();
|
|
|
|
// Put everyone into spectator state
|
|
if( !KFPCV.IsInState('Spectating') )
|
|
{
|
|
KFPCV.StartSpectate();
|
|
}
|
|
}
|
|
}
|
|
|
|
if( SpawnManager != none )
|
|
{
|
|
SpawnManager.ResetSpawnManager();
|
|
}
|
|
|
|
// Reset score statistics
|
|
WaveBonus = -1;
|
|
HumanDeaths = 0;
|
|
BossDamageDone = 0;
|
|
BossSurvivorDamageTaken = 0;
|
|
PercentOfZedsKilledBeforeWipe = 0.f;
|
|
|
|
// Reset pool manager (clear puke mines, c4, etc)
|
|
class'KFGameplayPoolManager'.static.GetPoolManager().Reset();
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* state RoundEnded
|
|
*********************************************************************************************/
|
|
|
|
/** Close the team results screen */
|
|
protected function ClosePostRoundMenu( optional bool bMatchOver=false )
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
|
|
foreach WorldInfo.AllControllers( class'KFPlayerControllerVersus', KFPCV )
|
|
{
|
|
KFPCV.ClientShowPostGameMenu( false );
|
|
}
|
|
|
|
// Wait a tiny bit to display the next round message
|
|
if( !bMatchOver )
|
|
{
|
|
SetTimer( 0.1, false, nameOf(Timer_AnnounceNextRound) );
|
|
}
|
|
}
|
|
|
|
/** Announces the next round to players */
|
|
protected function Timer_AnnounceNextRound()
|
|
{
|
|
local KFPlayerController KFPC;
|
|
|
|
foreach WorldInfo.AllControllers( class'KFPlayerController', KFPC )
|
|
{
|
|
// Only process players that are ready to play
|
|
if( KFPC.CanRestartPlayer() )
|
|
{
|
|
KFPC.ReceiveLocalizedMessage( class'KFLocalMessage_Priority', GMT_NextRoundBegin,,, KFPC.PlayerReplicationInfo.Team );
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Checks to ensure a game is actually playable and balances accordingly */
|
|
protected function CheckTeamNumbers()
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
local int HumanPlayers, ZedPlayers;
|
|
|
|
// If there aren't at least 2 players, end the match
|
|
if( NumPlayers < 2 || MyKFGRIV.CurrentRound > 1 )
|
|
{
|
|
GotoState( 'MatchEnded' );
|
|
return;
|
|
}
|
|
|
|
// If there are no players on one of the teams, force one of them to switch
|
|
foreach WorldInfo.AllControllers( class'KFPlayerControllerVersus', KFPCV )
|
|
{
|
|
if( !KFPCV.CanRestartPlayer() )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if( KFPCV.GetTeamNum() == 255 )
|
|
{
|
|
++ZedPlayers;
|
|
}
|
|
else
|
|
{
|
|
++HumanPlayers;
|
|
}
|
|
}
|
|
|
|
if( HumanPlayers == 0 && ZedPlayers > 1 )
|
|
{
|
|
SwitchOnePlayerToTeam( 0 );
|
|
}
|
|
else if( ZedPlayers == 0 && HumanPlayers > 1 )
|
|
{
|
|
SwitchOnePlayerToTeam( 255 );
|
|
}
|
|
}
|
|
|
|
/** Resets the level and announces a round change */
|
|
protected function BeginNextRound()
|
|
{
|
|
// Reset everything
|
|
ResetLevel();
|
|
|
|
// Init next round
|
|
MyKFGRIV.bStopCountDown = true;
|
|
MyKFGRIV.SetPlayerZedSpawnTime( PostRoundWaitTime, false );
|
|
|
|
ClosePostRoundMenu();
|
|
}
|
|
|
|
static function bool IsInitialSpawnPointSelection( WorldInfo WI )
|
|
{
|
|
local KFGameReplicationInfo KFGRI;
|
|
|
|
KFGRI = KFGameReplicationInfo( WI.GRI );
|
|
if( KFGRI != none )
|
|
{
|
|
return KFGRI.bRoundIsOver || super.IsInitialSpawnPointSelection( WI );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Attempts to pre-select player starts and begin texture streaming */
|
|
protected function PreSelectPlayerStarts()
|
|
{
|
|
local KFPlayerController KFPC;
|
|
local byte TeamNum;
|
|
|
|
foreach WorldInfo.AllControllers( class'KFPlayerController', KFPC )
|
|
{
|
|
KFPC.StartSpot = none;
|
|
TeamNum = KFPC.GetTeamNum();
|
|
if( KFPC.GetTeamNum() == 0 )
|
|
{
|
|
KFPC.StartSpot = FindPlayerStart( KFPC, TeamNum );
|
|
}
|
|
else
|
|
{
|
|
KFPC.StartSpot = none;
|
|
continue;
|
|
}
|
|
|
|
if( KFPC.StartSpot != none )
|
|
{
|
|
KFPC.ClientAddTextureStreamingLoc( KFPC.StartSpot.Location, 0.f, false );
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Starts spawning */
|
|
function StartSpawning()
|
|
{
|
|
// Reset round over flag
|
|
MyKFGRIV.bRoundIsOver = false;
|
|
MyKFGRIV.bNetDirty = true;
|
|
MyKFGRIV.bForceNetUpdate = true;
|
|
|
|
// Off we go!
|
|
GotoState( '' );
|
|
StartMatch();
|
|
}
|
|
|
|
/** Swaps one player to the opposite team */
|
|
protected function SwitchOnePlayerToTeam( byte TeamNum )
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
|
|
foreach WorldInfo.AllControllers( class'KFPlayerControllerVersus', KFPCV )
|
|
{
|
|
if( KFPCV.CanRestartPlayer()
|
|
&& KFPCV.GetTeamNum() != TeamNum )
|
|
{
|
|
SetTeam( KFPCV, TeamNum == 0 ? Teams[0] : Teams[1] );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
state RoundEnded
|
|
{
|
|
event BeginState( name PrevStateName )
|
|
{
|
|
local int i;
|
|
|
|
// Set round over flag (allows perk changes etc)
|
|
MyKFGRIV.bRoundIsOver = true;
|
|
|
|
// Increment round
|
|
MyKFGRIV.CurrentRound += 1;
|
|
|
|
// BWJ - 10-3-16 - Clear the spawned in flag when round end begins
|
|
for( i = 0; i < GameReplicationInfo.PRIArray.Length; i++ )
|
|
{
|
|
KFPlayerReplicationInfo(GameReplicationInfo.PRIArray[i]).bHasSpawnedIn = false;
|
|
}
|
|
|
|
// Turn off respawn timer
|
|
MyKFGRIV.SetPlayerZedSpawnTime( 255, false );
|
|
}
|
|
|
|
function bool MatchIsInProgress()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ForceEnded:
|
|
Sleep( 1.f );
|
|
BeginNextRound();
|
|
Goto( 'End' );
|
|
|
|
Begin:
|
|
// Wait a short time and bring up the results screen
|
|
Sleep( RoundEndCinematicDelay );
|
|
OpenPostRoundMenu();
|
|
|
|
// Wait for the results screen duration and start the next round
|
|
Sleep( GetEndOfMatchTime() );
|
|
if( MyKFGRIV.CurrentRound > 1 )
|
|
{
|
|
GotoState( 'MatchEnded' );
|
|
Sleep(0.f);
|
|
}
|
|
CheckTeamNumbers();
|
|
BeginNextRound();
|
|
|
|
// Wait a short time after closing the results screen (for perk changes) before spawning
|
|
Sleep( fMax(PostRoundWaitTime - 3.f, 0.f) );
|
|
PreSelectPlayerStarts();
|
|
Sleep( 3.f );
|
|
|
|
End:
|
|
StartSpawning();
|
|
}
|
|
|
|
protected function CheckRoundEndAchievements( byte WinningTeam )
|
|
{
|
|
local KFPlayerControllerVersus KFPCV;
|
|
|
|
foreach WorldInfo.AllControllers( class'KFPlayerControllerVersus', KFPCV )
|
|
{
|
|
KFPCV.ClientRoundEnded( WinningTeam );
|
|
}
|
|
}
|
|
|
|
function TryRestartGame()
|
|
{
|
|
if( IsInState('RoundEnded') )
|
|
{
|
|
BeginNextRound();
|
|
}
|
|
else
|
|
{
|
|
super.TryRestartGame();
|
|
}
|
|
}
|
|
|
|
function ShowPostGameMenu()
|
|
{
|
|
ClosePostRoundMenu( true );
|
|
|
|
super.ShowPostGameMenu();
|
|
}
|
|
|
|
function float GetEndOfMatchTime()
|
|
{
|
|
if( IsInState('RoundEnded') )
|
|
{
|
|
return TimeUntilNextRound;
|
|
}
|
|
|
|
return super.GetEndOfMatchTime();
|
|
}
|
|
|
|
function bool IsMapObjectiveEnabled()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
DefaultProperties
|
|
{
|
|
bIsVersusGame=true
|
|
TimeUntilNextRound=12
|
|
RoundEndCinematicDelay=4
|
|
PostRoundWaitTime=15
|
|
MaxGameDifficulty=0
|
|
|
|
GameLengthDoshScale(0)=1.0 // Short but already scaled in other ways for versus
|
|
MaxRespawnDosh(0)=1950 // Max amount of dosh that a player can respawn with after dying //1750
|
|
DeathPenaltyModifiers(0)=0.03 // How much of a penalty to apply to dosh after dying. Smaller means lower penalty //0.05
|
|
|
|
OnlineGameSettingsClass=class'KFGame.KFOnlineGameSettingsVersus'
|
|
HUDType=class'KFGameContent.KFGFXHudWrapper_Versus'
|
|
KFGFxManagerClass=class'KFGame.KFGFxMoviePlayer_Manager_Versus'
|
|
PlayerControllerClass=class'KFGameContent.KFPlayerControllerVersus'
|
|
PlayerReplicationInfoClass=class'KFGame.KFPlayerReplicationInfoVersus'
|
|
GameReplicationInfoClass=class'KFGameContent.KFGameReplicationInfoVersus'
|
|
MaxPlayersAllowed=`KF_MAX_PLAYERS_VERSUS
|
|
DefaultPawnClass=class'KFGameContent.KFPawn_Human_Versus'
|
|
GameConductorClass=class'KFGameContent.KFGameConductorVersus'
|
|
DifficultyInfoClass=class'KFGameDifficulty_Versus'
|
|
DifficultyInfoConsoleClass=class'KFGameDifficulty_Versus_Console'
|
|
|
|
PlayerZedClasses(AT_AlphaClot)=class'KFGameContent.KFPawn_ZedClot_Alpha_Versus'
|
|
PlayerZedClasses(AT_SlasherClot)=class'KFGameContent.KFPawn_ZedClot_Slasher_Versus'
|
|
PlayerZedClasses(AT_Crawler)=class'KFGameContent.KFPawn_ZedCrawler_Versus'
|
|
PlayerZedClasses(AT_GoreFast)=class'KFGameContent.KFPawn_ZedGorefast_Versus'
|
|
PlayerZedClasses(AT_Stalker)=class'KFGameContent.KFPawn_ZedStalker_Versus'
|
|
PlayerZedClasses(AT_Scrake)=class'KFGameContent.KFPawn_ZedScrake_Versus'
|
|
PlayerZedClasses(AT_FleshPound)=class'KFGameContent.KFPawn_ZedFleshpound_Versus'
|
|
PlayerZedClasses(AT_Bloat)=class'KFGameContent.KFPawn_ZedBloat_Versus'
|
|
PlayerZedClasses(AT_Siren)=class'KFGameContent.KFPawn_ZedSiren_Versus'
|
|
PlayerZedClasses(AT_Husk)=class'KFGameContent.KFPawn_ZedHusk_Versus'
|
|
|
|
PlayerBossClassList.Add(class'KFGameContent.KFPawn_ZedPatriarch_Versus')
|
|
|
|
SpawnManagerClasses.Empty
|
|
SpawnManagerClasses(0)=class'KFAISpawnManager_Versus'
|
|
|
|
AntiGriefDamageTypeClass=class'KFDT_NoGoVolume'
|
|
|
|
/** Anti-griefing system */
|
|
ANTI_GRIEF_DELAY=30.f
|
|
ANTI_GRIEF_INTERVAL=2.f
|
|
ANTI_GRIEF_DAMAGE_PERCENTAGE=10.f
|
|
}
|