1039 lines
33 KiB
Plaintext
1039 lines
33 KiB
Plaintext
untyped
|
|
|
|
global function PIN_GameStart
|
|
global function SetGameState
|
|
global function GameState_EntitiesDidLoad
|
|
global function WaittillGameStateOrHigher
|
|
global function AddCallback_OnRoundEndCleanup
|
|
|
|
global function SetShouldUsePickLoadoutScreen
|
|
global function SetSwitchSidesBased
|
|
global function SetSuddenDeathBased
|
|
global function SetTimerBased
|
|
global function SetShouldUseRoundWinningKillReplay
|
|
global function SetRoundWinningKillReplayKillClasses
|
|
global function SetRoundWinningKillReplayAttacker
|
|
global function SetCallback_TryUseProjectileReplay
|
|
global function ShouldTryUseProjectileReplay
|
|
global function SetWinner
|
|
global function SetTimeoutWinnerDecisionFunc
|
|
global function AddTeamScore
|
|
global function GetWinningTeamWithFFASupport
|
|
|
|
global function GameState_GetTimeLimitOverride
|
|
global function IsRoundBasedGameOver
|
|
global function ShouldRunEvac
|
|
global function GiveTitanToPlayer
|
|
global function GetTimeLimit_ForGameMode
|
|
|
|
struct {
|
|
// used for togglable parts of gamestate
|
|
bool usePickLoadoutScreen
|
|
bool switchSidesBased
|
|
bool suddenDeathBased
|
|
bool timerBased = true
|
|
int functionref() timeoutWinnerDecisionFunc
|
|
|
|
// for waitingforplayers
|
|
int numPlayersFullyConnected
|
|
|
|
bool hasSwitchedSides
|
|
|
|
int announceRoundWinnerWinningSubstr
|
|
int announceRoundWinnerLosingSubstr
|
|
|
|
bool roundWinningKillReplayTrackPilotKills = true
|
|
bool roundWinningKillReplayTrackTitanKills = false
|
|
|
|
bool gameWonThisFrame
|
|
bool hasKillForGameWonThisFrame
|
|
float roundWinningKillReplayTime
|
|
entity roundWinningKillReplayVictim
|
|
entity roundWinningKillReplayAttacker
|
|
int roundWinningKillReplayInflictorEHandle // this is either the inflictor or the attacker
|
|
int roundWinningKillReplayMethodOfDeath
|
|
float roundWinningKillReplayTimeOfDeath
|
|
float roundWinningKillReplayHealthFrac
|
|
|
|
array<void functionref()> roundEndCleanupCallbacks
|
|
bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) shouldTryUseProjectileReplayCallback
|
|
} file
|
|
|
|
void function SetCallback_TryUseProjectileReplay( bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) callback )
|
|
{
|
|
file.shouldTryUseProjectileReplayCallback = callback
|
|
}
|
|
|
|
bool function ShouldTryUseProjectileReplay( entity victim, entity attacker, var damageInfo, bool isRoundEnd )
|
|
{
|
|
if ( file.shouldTryUseProjectileReplayCallback != null )
|
|
return file.shouldTryUseProjectileReplayCallback( victim, attacker, damageInfo, isRoundEnd )
|
|
// default to true (vanilla behaviour)
|
|
return true
|
|
}
|
|
|
|
void function PIN_GameStart()
|
|
{
|
|
// todo: using the pin telemetry function here, weird and was done veeery early on before i knew how this all worked, should use a different one
|
|
|
|
// called from InitGameState
|
|
//FlagInit( "ReadyToStartMatch" )
|
|
|
|
SetServerVar( "switchedSides", 0 )
|
|
SetServerVar( "winningTeam", -1 )
|
|
|
|
AddCallback_GameStateEnter( eGameState.WaitingForCustomStart, GameStateEnter_WaitingForCustomStart )
|
|
AddCallback_GameStateEnter( eGameState.WaitingForPlayers, GameStateEnter_WaitingForPlayers )
|
|
AddCallback_OnClientConnected( WaitingForPlayers_ClientConnected )
|
|
|
|
AddCallback_GameStateEnter( eGameState.PickLoadout, GameStateEnter_PickLoadout )
|
|
AddCallback_GameStateEnter( eGameState.Prematch, GameStateEnter_Prematch )
|
|
AddCallback_GameStateEnter( eGameState.Playing, GameStateEnter_Playing )
|
|
AddCallback_GameStateEnter( eGameState.WinnerDetermined, GameStateEnter_WinnerDetermined )
|
|
AddCallback_GameStateEnter( eGameState.SwitchingSides, GameStateEnter_SwitchingSides )
|
|
AddCallback_GameStateEnter( eGameState.SuddenDeath, GameStateEnter_SuddenDeath )
|
|
AddCallback_GameStateEnter( eGameState.Postmatch, GameStateEnter_Postmatch )
|
|
|
|
AddCallback_OnPlayerKilled( OnPlayerKilled )
|
|
AddDeathCallback( "npc_titan", OnTitanKilled )
|
|
AddCallback_EntityChangedTeam( "player", OnPlayerChangedTeam )
|
|
|
|
RegisterSignal( "CleanUpEntitiesForRoundEnd" )
|
|
}
|
|
|
|
void function SetGameState( int newState )
|
|
{
|
|
if ( newState == GetGameState() )
|
|
return
|
|
|
|
SetServerVar( "gameStateChangeTime", Time() )
|
|
SetServerVar( "gameState", newState )
|
|
svGlobal.levelEnt.Signal( "GameStateChanged" )
|
|
|
|
// added in AddCallback_GameStateEnter
|
|
foreach ( callbackFunc in svGlobal.gameStateEnterCallbacks[ newState ] )
|
|
callbackFunc()
|
|
}
|
|
|
|
void function GameState_EntitiesDidLoad()
|
|
{
|
|
if ( GetClassicMPMode() || ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() )
|
|
ClassicMP_SetupIntro()
|
|
}
|
|
|
|
void function WaittillGameStateOrHigher( int gameState )
|
|
{
|
|
while ( GetGameState() < gameState )
|
|
svGlobal.levelEnt.WaitSignal( "GameStateChanged" )
|
|
}
|
|
|
|
|
|
// logic for individual gamestates:
|
|
|
|
|
|
// eGameState.WaitingForCustomStart
|
|
void function GameStateEnter_WaitingForCustomStart()
|
|
{
|
|
// unused in release, comments indicate this was supposed to be used for an e3 demo
|
|
// perhaps games in this demo were manually started by an employee? no clue really
|
|
}
|
|
|
|
|
|
// eGameState.WaitingForPlayers
|
|
void function GameStateEnter_WaitingForPlayers()
|
|
{
|
|
foreach ( entity player in GetPlayerArray() )
|
|
WaitingForPlayers_ClientConnected( player )
|
|
|
|
thread WaitForPlayers() // like 90% sure there should be a way to get number of loading clients on server but idk it
|
|
}
|
|
|
|
void function WaitForPlayers( )
|
|
{
|
|
// note: atm if someone disconnects as this happens the game will just wait forever
|
|
float endTime = Time() + 30.0
|
|
|
|
while ( ( GetPendingClientsCount() != 0 && endTime > Time() ) || GetPlayerArray().len() == 0 )
|
|
WaitFrame()
|
|
|
|
print( "done waiting!" )
|
|
|
|
wait 1.0 // bit nicer
|
|
if ( file.usePickLoadoutScreen )
|
|
SetGameState( eGameState.PickLoadout )
|
|
else
|
|
SetGameState( eGameState.Prematch )
|
|
}
|
|
|
|
void function WaitingForPlayers_ClientConnected( entity player )
|
|
{
|
|
if ( GetGameState() == eGameState.WaitingForPlayers )
|
|
ScreenFadeToBlackForever( player, 0.0 )
|
|
}
|
|
|
|
// eGameState.PickLoadout
|
|
void function GameStateEnter_PickLoadout()
|
|
{
|
|
thread GameStateEnter_PickLoadout_Threaded()
|
|
}
|
|
|
|
void function GameStateEnter_PickLoadout_Threaded()
|
|
{
|
|
float pickloadoutLength = 20.0 // may need tweaking
|
|
SetServerVar( "minPickLoadOutTime", Time() + pickloadoutLength )
|
|
|
|
// titan selection menu can change minPickLoadOutTime so we need to wait manually until we hit the time
|
|
while ( Time() < GetServerVar( "minPickLoadOutTime" ) )
|
|
WaitFrame()
|
|
|
|
SetGameState( eGameState.Prematch )
|
|
}
|
|
|
|
|
|
// eGameState.Prematch
|
|
void function GameStateEnter_Prematch()
|
|
{
|
|
int timeLimit = GameMode_GetTimeLimit( GAMETYPE ) * 60
|
|
if ( file.switchSidesBased )
|
|
timeLimit /= 2 // endtime is half of total per side
|
|
|
|
SetServerVar( "gameEndTime", Time() + timeLimit + ClassicMP_GetIntroLength() )
|
|
SetServerVar( "roundEndTime", Time() + ClassicMP_GetIntroLength() + GameMode_GetRoundTimeLimit( GAMETYPE ) * 60 )
|
|
|
|
if ( !GetClassicMPMode() && !ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() )
|
|
thread StartGameWithoutClassicMP()
|
|
|
|
// Initialise any spectators. Hopefully they are all initialised already in CodeCallback_OnClientConnectionCompleted
|
|
// (_base_gametype_mp.gnut) but for modes like LTS this doesn't seem to happen late enough to work properly.
|
|
foreach ( player in GetPlayerArray() )
|
|
{
|
|
if ( IsPrivateMatchSpectator( player ) )
|
|
InitialisePrivateMatchSpectatorPlayer( player )
|
|
}
|
|
}
|
|
|
|
void function StartGameWithoutClassicMP()
|
|
{
|
|
foreach ( entity player in GetPlayerArray() )
|
|
if ( IsAlive( player ) )
|
|
player.Die()
|
|
|
|
WaitFrame() // wait for callbacks to finish
|
|
|
|
// need these otherwise game will complain
|
|
SetServerVar( "gameStartTime", Time() )
|
|
SetServerVar( "roundStartTime", Time() )
|
|
|
|
foreach ( entity player in GetPlayerArray() )
|
|
{
|
|
if ( !IsPrivateMatchSpectator( player ) )
|
|
RespawnAsPilot( player )
|
|
|
|
ScreenFadeFromBlack( player, 0 )
|
|
}
|
|
|
|
SetGameState( eGameState.Playing )
|
|
}
|
|
|
|
|
|
// eGameState.Playing
|
|
void function GameStateEnter_Playing()
|
|
{
|
|
thread GameStateEnter_Playing_Threaded()
|
|
}
|
|
|
|
void function GameStateEnter_Playing_Threaded()
|
|
{
|
|
WaitFrame() // ensure timelimits are all properly set
|
|
|
|
thread DialoguePlayNormal() // runs dialogue play function
|
|
|
|
while ( GetGameState() == eGameState.Playing )
|
|
{
|
|
// could cache these, but what if we update it midgame?
|
|
float endTime
|
|
if ( IsRoundBased() )
|
|
endTime = expect float( GetServerVar( "roundEndTime" ) )
|
|
else
|
|
endTime = expect float( GetServerVar( "gameEndTime" ) )
|
|
|
|
// time's up!
|
|
if ( Time() >= endTime && file.timerBased )
|
|
{
|
|
int winningTeam
|
|
if ( file.timeoutWinnerDecisionFunc != null )
|
|
winningTeam = file.timeoutWinnerDecisionFunc()
|
|
else
|
|
winningTeam = GetWinningTeamWithFFASupport()
|
|
|
|
if ( file.switchSidesBased && !file.hasSwitchedSides && !IsRoundBased() ) // in roundbased modes, we handle this in setwinner
|
|
SetGameState( eGameState.SwitchingSides )
|
|
else if ( file.suddenDeathBased && winningTeam == TEAM_UNASSIGNED ) // suddendeath if we draw and suddendeath is enabled and haven't switched sides
|
|
SetGameState( eGameState.SuddenDeath )
|
|
else
|
|
SetWinner( winningTeam )
|
|
}
|
|
|
|
WaitFrame()
|
|
}
|
|
}
|
|
|
|
|
|
// eGameState.WinnerDetermined
|
|
// these are likely innacurate
|
|
const float ROUND_END_FADE_KILLREPLAY = 1.0
|
|
const float ROUND_END_DELAY_KILLREPLAY = 3.0
|
|
const float ROUND_END_FADE_NOKILLREPLAY = 8.0
|
|
const float ROUND_END_DELAY_NOKILLREPLAY = 10.0
|
|
|
|
void function GameStateEnter_WinnerDetermined()
|
|
{
|
|
thread GameStateEnter_WinnerDetermined_Threaded()
|
|
}
|
|
|
|
void function GameStateEnter_WinnerDetermined_Threaded()
|
|
{
|
|
// do win announcement
|
|
int winningTeam = GetWinningTeamWithFFASupport()
|
|
|
|
DialoguePlayWinnerDetermined() // play a faction dialogue when winner is determined
|
|
|
|
foreach ( entity player in GetPlayerArray() )
|
|
{
|
|
int announcementSubstr
|
|
if ( winningTeam != TEAM_UNASSIGNED )
|
|
announcementSubstr = player.GetTeam() == winningTeam ? file.announceRoundWinnerWinningSubstr : file.announceRoundWinnerLosingSubstr
|
|
|
|
if ( IsRoundBased() )
|
|
Remote_CallFunction_NonReplay( player, "ServerCallback_AnnounceRoundWinner", winningTeam, announcementSubstr, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME, GameRules_GetTeamScore2( TEAM_MILITIA ), GameRules_GetTeamScore2( TEAM_IMC ) )
|
|
else
|
|
Remote_CallFunction_NonReplay( player, "ServerCallback_AnnounceWinner", winningTeam, announcementSubstr, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
|
|
|
|
if ( player.GetTeam() == winningTeam )
|
|
UnlockAchievement( player, achievements.MP_WIN )
|
|
}
|
|
|
|
WaitFrame() // wait a frame so other scripts can setup killreplay stuff
|
|
|
|
// set gameEndTime to current time, so hud doesn't display time left in the match
|
|
SetServerVar( "gameEndTime", Time() )
|
|
SetServerVar( "roundEndTime", Time() )
|
|
|
|
entity replayAttacker = file.roundWinningKillReplayAttacker
|
|
bool doReplay = Replay_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker ) && !ClassicMP_ShouldRunEpilogue()
|
|
&& Time() - file.roundWinningKillReplayTime <= ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY && winningTeam != TEAM_UNASSIGNED
|
|
|
|
float replayLength = 2.0 // extra delay if no replay
|
|
if ( doReplay )
|
|
{
|
|
bool killcamsWereEnabled = KillcamsEnabled()
|
|
if ( killcamsWereEnabled ) // dont want killcams to interrupt stuff
|
|
SetKillcamsEnabled( false )
|
|
|
|
replayLength = ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY
|
|
if ( "respawnTime" in replayAttacker.s && Time() - replayAttacker.s.respawnTime < replayLength )
|
|
replayLength += Time() - expect float ( replayAttacker.s.respawnTime )
|
|
|
|
SetServerVar( "roundWinningKillReplayEntHealthFrac", file.roundWinningKillReplayHealthFrac )
|
|
|
|
foreach ( entity player in GetPlayerArray() )
|
|
thread PlayerWatchesRoundWinningKillReplay( player, replayLength )
|
|
|
|
wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
|
|
CleanUpEntitiesForRoundEnd() // fade should be done by this point, so cleanup stuff now when people won't see
|
|
wait replayLength
|
|
|
|
WaitFrame() // prevent a race condition with PlayerWatchesRoundWinningKillReplay
|
|
file.roundWinningKillReplayAttacker = null // clear this
|
|
file.roundWinningKillReplayInflictorEHandle = -1
|
|
|
|
if ( killcamsWereEnabled )
|
|
SetKillcamsEnabled( true )
|
|
}
|
|
else if ( IsRoundBased() || !ClassicMP_ShouldRunEpilogue() )
|
|
{
|
|
// these numbers are temp and should really be based on consts of some kind
|
|
foreach( entity player in GetPlayerArray() )
|
|
{
|
|
player.FreezeControlsOnServer()
|
|
ScreenFadeToBlackForever( player, 4.0 )
|
|
}
|
|
|
|
wait ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY
|
|
CleanUpEntitiesForRoundEnd() // fade should be done by this point, so cleanup stuff now when people won't see
|
|
|
|
foreach( entity player in GetPlayerArray() )
|
|
player.UnfreezeControlsOnServer()
|
|
}
|
|
|
|
if ( IsRoundBased() )
|
|
{
|
|
svGlobal.levelEnt.Signal( "RoundEnd" )
|
|
int roundsPlayed = expect int ( GetServerVar( "roundsPlayed" ) )
|
|
SetServerVar( "roundsPlayed", roundsPlayed + 1 )
|
|
|
|
int winningTeam = GetWinningTeamWithFFASupport()
|
|
|
|
int highestScore = GameRules_GetTeamScore( winningTeam )
|
|
int roundScoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE )
|
|
|
|
if ( highestScore >= roundScoreLimit )
|
|
{
|
|
if ( ClassicMP_ShouldRunEpilogue() )
|
|
{
|
|
ClassicMP_SetupEpilogue()
|
|
SetGameState( eGameState.Epilogue )
|
|
}
|
|
else
|
|
SetGameState( eGameState.Postmatch )
|
|
}
|
|
else if ( file.switchSidesBased && !file.hasSwitchedSides && highestScore >= ( roundScoreLimit.tofloat() / 2.0 ) ) // round up
|
|
SetGameState( eGameState.SwitchingSides ) // note: switchingsides will handle setting to pickloadout and prematch by itself
|
|
else if ( file.usePickLoadoutScreen )
|
|
SetGameState( eGameState.PickLoadout )
|
|
else
|
|
SetGameState ( eGameState.Prematch )
|
|
}
|
|
else
|
|
{
|
|
if ( ClassicMP_ShouldRunEpilogue() )
|
|
{
|
|
ClassicMP_SetupEpilogue()
|
|
SetGameState( eGameState.Epilogue )
|
|
}
|
|
else
|
|
SetGameState( eGameState.Postmatch )
|
|
}
|
|
}
|
|
|
|
void function PlayerWatchesRoundWinningKillReplay( entity player, float replayLength )
|
|
{
|
|
// end if player dcs
|
|
player.EndSignal( "OnDestroy" )
|
|
|
|
player.FreezeControlsOnServer()
|
|
ScreenFadeToBlackForever( player, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
|
|
wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
|
|
|
|
player.SetPredictionEnabled( false ) // prediction fucks with replays
|
|
|
|
entity attacker = file.roundWinningKillReplayAttacker
|
|
if ( IsValid( attacker ) )
|
|
{
|
|
player.SetKillReplayDelay( Time() - replayLength, THIRD_PERSON_KILL_REPLAY_ALWAYS )
|
|
player.SetKillReplayInflictorEHandle( file.roundWinningKillReplayInflictorEHandle )
|
|
player.SetKillReplayVictim( file.roundWinningKillReplayVictim )
|
|
player.SetViewIndex( attacker.GetIndexForEntity() )
|
|
player.SetIsReplayRoundWinning( true )
|
|
}
|
|
|
|
if ( replayLength >= ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY - 0.5 ) // only do fade if close to full length replay
|
|
{
|
|
// this doesn't work because fades don't work on players that are in a replay, unsure how official servers do this
|
|
wait replayLength - 2.0
|
|
ScreenFadeToBlackForever( player, 2.0 )
|
|
|
|
wait 2.0
|
|
}
|
|
else
|
|
wait replayLength
|
|
|
|
//player.SetPredictionEnabled( true ) doesn't seem needed, as native code seems to set this on respawn
|
|
player.ClearReplayDelay()
|
|
player.ClearViewEntity()
|
|
player.UnfreezeControlsOnServer()
|
|
}
|
|
|
|
|
|
// eGameState.SwitchingSides
|
|
void function GameStateEnter_SwitchingSides()
|
|
{
|
|
thread GameStateEnter_SwitchingSides_Threaded()
|
|
}
|
|
|
|
void function GameStateEnter_SwitchingSides_Threaded()
|
|
{
|
|
bool killcamsWereEnabled = KillcamsEnabled()
|
|
if ( killcamsWereEnabled ) // dont want killcams to interrupt stuff
|
|
SetKillcamsEnabled( false )
|
|
|
|
WaitFrame() // wait a frame so callbacks can set killreplay info
|
|
|
|
entity replayAttacker = file.roundWinningKillReplayAttacker
|
|
bool doReplay = Replay_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker ) && !IsRoundBased() // for roundbased modes, we've already done the replay
|
|
&& Time() - file.roundWinningKillReplayTime <= SWITCHING_SIDES_DELAY
|
|
|
|
float replayLength = SWITCHING_SIDES_DELAY_REPLAY // extra delay if no replay
|
|
if ( doReplay )
|
|
{
|
|
replayLength = SWITCHING_SIDES_DELAY
|
|
if ( "respawnTime" in replayAttacker.s && Time() - replayAttacker.s.respawnTime < replayLength )
|
|
replayLength += Time() - expect float ( replayAttacker.s.respawnTime )
|
|
|
|
SetServerVar( "roundWinningKillReplayEntHealthFrac", file.roundWinningKillReplayHealthFrac )
|
|
}
|
|
|
|
foreach ( entity player in GetPlayerArray() )
|
|
thread PlayerWatchesSwitchingSidesKillReplay( player, doReplay, replayLength )
|
|
|
|
wait SWITCHING_SIDES_DELAY_REPLAY
|
|
CleanUpEntitiesForRoundEnd() // fade should be done by this point, so cleanup stuff now when people won't see
|
|
wait replayLength
|
|
|
|
if ( killcamsWereEnabled )
|
|
SetKillcamsEnabled( true )
|
|
|
|
file.hasSwitchedSides = true
|
|
svGlobal.levelEnt.Signal( "RoundEnd" ) // might be good to get a new signal for this? not 100% necessary tho i think
|
|
SetServerVar( "switchedSides", 1 )
|
|
file.roundWinningKillReplayAttacker = null // reset this after replay
|
|
file.roundWinningKillReplayInflictorEHandle = -1
|
|
|
|
if ( file.usePickLoadoutScreen )
|
|
SetGameState( eGameState.PickLoadout )
|
|
else
|
|
SetGameState ( eGameState.Prematch )
|
|
}
|
|
|
|
void function PlayerWatchesSwitchingSidesKillReplay( entity player, bool doReplay, float replayLength )
|
|
{
|
|
player.EndSignal( "OnDestroy" )
|
|
player.FreezeControlsOnServer()
|
|
|
|
ScreenFadeToBlackForever( player, SWITCHING_SIDES_DELAY_REPLAY ) // automatically cleared
|
|
wait SWITCHING_SIDES_DELAY_REPLAY
|
|
|
|
if ( doReplay )
|
|
{
|
|
player.SetPredictionEnabled( false ) // prediction fucks with replays
|
|
|
|
// delay seems weird for switchingsides? ends literally the frame the flag is collected
|
|
|
|
entity attacker = file.roundWinningKillReplayAttacker
|
|
player.SetKillReplayDelay( Time() - replayLength, THIRD_PERSON_KILL_REPLAY_ALWAYS )
|
|
player.SetKillReplayInflictorEHandle( file.roundWinningKillReplayInflictorEHandle )
|
|
player.SetKillReplayVictim( file.roundWinningKillReplayVictim )
|
|
player.SetViewIndex( attacker.GetIndexForEntity() )
|
|
player.SetIsReplayRoundWinning( true )
|
|
|
|
if ( replayLength >= SWITCHING_SIDES_DELAY - 0.5 ) // only do fade if close to full length replay
|
|
{
|
|
// this doesn't work because fades don't work on players that are in a replay, unsure how official servers do this
|
|
wait replayLength - ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
|
|
ScreenFadeToBlackForever( player, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
|
|
|
|
wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
|
|
}
|
|
else
|
|
wait replayLength
|
|
}
|
|
else
|
|
wait SWITCHING_SIDES_DELAY_REPLAY // extra delay if no replay
|
|
|
|
//player.SetPredictionEnabled( true ) doesn't seem needed, as native code seems to set this on respawn
|
|
player.ClearReplayDelay()
|
|
player.ClearViewEntity()
|
|
}
|
|
|
|
|
|
// eGameState.SuddenDeath
|
|
void function GameStateEnter_SuddenDeath()
|
|
{
|
|
// disable respawns, suddendeath calling is done on a kill callback
|
|
SetRespawnsEnabled( false )
|
|
|
|
// defensive fixes, so game won't stuck in SuddenDeath forever
|
|
bool mltElimited = false
|
|
bool imcElimited = false
|
|
if( GetPlayerArrayOfTeam_Alive( TEAM_MILITIA ).len() < 1 )
|
|
mltElimited = true
|
|
if( GetPlayerArrayOfTeam_Alive( TEAM_IMC ).len() < 1 )
|
|
imcElimited = true
|
|
if( mltElimited && imcElimited )
|
|
SetWinner( TEAM_UNASSIGNED )
|
|
else if( mltElimited )
|
|
SetWinner( TEAM_IMC )
|
|
else if( imcElimited )
|
|
SetWinner( TEAM_MILITIA )
|
|
}
|
|
|
|
|
|
// eGameState.Postmatch
|
|
void function GameStateEnter_Postmatch()
|
|
{
|
|
foreach ( entity player in GetPlayerArray() )
|
|
{
|
|
player.FreezeControlsOnServer()
|
|
thread ForceFadeToBlack( player )
|
|
}
|
|
|
|
thread GameStateEnter_Postmatch_Threaded()
|
|
}
|
|
|
|
void function GameStateEnter_Postmatch_Threaded()
|
|
{
|
|
wait GAME_POSTMATCH_LENGTH
|
|
|
|
GameRules_EndMatch()
|
|
}
|
|
|
|
void function ForceFadeToBlack( entity player )
|
|
{
|
|
// todo: check if this is still necessary
|
|
player.EndSignal( "OnDestroy" )
|
|
|
|
// hack until i figure out what deathcam stuff is causing fadetoblacks to be cleared
|
|
while ( true )
|
|
{
|
|
WaitFrame()
|
|
ScreenFadeToBlackForever( player, 0.0 )
|
|
}
|
|
}
|
|
|
|
|
|
// shared across multiple gamestates
|
|
|
|
void function OnPlayerKilled( entity victim, entity attacker, var damageInfo )
|
|
{
|
|
if ( !GamePlayingOrSuddenDeath() )
|
|
{
|
|
if ( file.gameWonThisFrame )
|
|
{
|
|
if ( file.hasKillForGameWonThisFrame )
|
|
return
|
|
}
|
|
else
|
|
return
|
|
}
|
|
|
|
entity inflictor = DamageInfo_GetInflictor( damageInfo )
|
|
bool shouldUseInflictor = IsValid( inflictor ) && ShouldTryUseProjectileReplay( victim, attacker, damageInfo, true )
|
|
|
|
// set round winning killreplay info here if we're tracking pilot kills
|
|
// todo: make this not count environmental deaths like falls, unsure how to prevent this
|
|
if ( file.roundWinningKillReplayTrackPilotKills && victim != attacker && attacker != svGlobal.worldspawn && IsValid( attacker ) )
|
|
{
|
|
if ( file.gameWonThisFrame )
|
|
file.hasKillForGameWonThisFrame = true
|
|
file.roundWinningKillReplayTime = Time()
|
|
file.roundWinningKillReplayVictim = victim
|
|
file.roundWinningKillReplayAttacker = attacker
|
|
file.roundWinningKillReplayInflictorEHandle = ( shouldUseInflictor ? inflictor : attacker ).GetEncodedEHandle()
|
|
file.roundWinningKillReplayMethodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo )
|
|
file.roundWinningKillReplayTimeOfDeath = Time()
|
|
file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
|
|
}
|
|
|
|
if ( ( Riff_EliminationMode() == eEliminationMode.Titans || Riff_EliminationMode() == eEliminationMode.PilotsTitans ) && victim.IsTitan() ) // need an extra check for this
|
|
OnTitanKilled( victim, damageInfo )
|
|
|
|
if ( !GamePlayingOrSuddenDeath() )
|
|
return
|
|
|
|
// note: pilotstitans is just win if enemy team runs out of either pilots or titans
|
|
if ( IsPilotEliminationBased() || GetGameState() == eGameState.SuddenDeath )
|
|
{
|
|
if ( GetPlayerArrayOfTeam_Alive( victim.GetTeam() ).len() == 0 )
|
|
{
|
|
// for ffa we need to manually get the last team alive
|
|
if ( IsFFAGame() )
|
|
{
|
|
array<int> teamsWithLivingPlayers
|
|
foreach ( entity player in GetPlayerArray_Alive() )
|
|
{
|
|
if ( !teamsWithLivingPlayers.contains( player.GetTeam() ) )
|
|
teamsWithLivingPlayers.append( player.GetTeam() )
|
|
}
|
|
|
|
if ( teamsWithLivingPlayers.len() == 1 )
|
|
SetWinner( teamsWithLivingPlayers[ 0 ], "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" )
|
|
else if ( teamsWithLivingPlayers.len() == 0 ) // failsafe: only team was the dead one
|
|
SetWinner( TEAM_UNASSIGNED, "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" ) // this is fine in ffa
|
|
}
|
|
else
|
|
SetWinner( GetOtherTeam( victim.GetTeam() ), "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" )
|
|
}
|
|
}
|
|
}
|
|
|
|
void function OnTitanKilled( entity victim, var damageInfo )
|
|
{
|
|
if ( !GamePlayingOrSuddenDeath() )
|
|
{
|
|
if ( file.gameWonThisFrame )
|
|
{
|
|
if ( file.hasKillForGameWonThisFrame )
|
|
return
|
|
}
|
|
else
|
|
return
|
|
}
|
|
|
|
entity inflictor = DamageInfo_GetInflictor( damageInfo )
|
|
bool shouldUseInflictor = IsValid( inflictor ) && ShouldTryUseProjectileReplay( victim, DamageInfo_GetAttacker( damageInfo ), damageInfo, true )
|
|
|
|
// set round winning killreplay info here if we're tracking titan kills
|
|
// todo: make this not count environmental deaths like falls, unsure how to prevent this
|
|
entity attacker = DamageInfo_GetAttacker( damageInfo )
|
|
if ( file.roundWinningKillReplayTrackTitanKills && victim != attacker && attacker != svGlobal.worldspawn && IsValid( attacker ) )
|
|
{
|
|
if ( file.gameWonThisFrame )
|
|
file.hasKillForGameWonThisFrame = true
|
|
file.roundWinningKillReplayTime = Time()
|
|
file.roundWinningKillReplayVictim = victim
|
|
file.roundWinningKillReplayAttacker = attacker
|
|
file.roundWinningKillReplayInflictorEHandle = ( shouldUseInflictor ? inflictor : attacker ).GetEncodedEHandle()
|
|
file.roundWinningKillReplayMethodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo )
|
|
file.roundWinningKillReplayTimeOfDeath = Time()
|
|
file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
|
|
}
|
|
|
|
if ( !GamePlayingOrSuddenDeath() )
|
|
return
|
|
|
|
// note: pilotstitans is just win if enemy team runs out of either pilots or titans
|
|
if ( IsTitanEliminationBased() )
|
|
{
|
|
int livingTitans
|
|
foreach ( entity titan in GetTitanArrayOfTeam( victim.GetTeam() ) )
|
|
livingTitans++
|
|
|
|
if ( livingTitans == 0 )
|
|
{
|
|
// for ffa we need to manually get the last team alive
|
|
if ( IsFFAGame() )
|
|
{
|
|
array<int> teamsWithLivingTitans
|
|
foreach ( entity titan in GetTitanArray() )
|
|
{
|
|
if ( !teamsWithLivingTitans.contains( titan.GetTeam() ) )
|
|
teamsWithLivingTitans.append( titan.GetTeam() )
|
|
}
|
|
|
|
if ( teamsWithLivingTitans.len() == 1 )
|
|
SetWinner( teamsWithLivingTitans[ 0 ], "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" )
|
|
else if ( teamsWithLivingTitans.len() == 0 ) // failsafe: only team was the dead one
|
|
SetWinner( TEAM_UNASSIGNED, "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" ) // this is fine in ffa
|
|
}
|
|
else
|
|
SetWinner( GetOtherTeam( victim.GetTeam() ), "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" )
|
|
}
|
|
}
|
|
}
|
|
|
|
void function AddCallback_OnRoundEndCleanup( void functionref() callback )
|
|
{
|
|
file.roundEndCleanupCallbacks.append( callback )
|
|
}
|
|
|
|
void function CleanUpEntitiesForRoundEnd()
|
|
{
|
|
// this function should clean up any and all entities that need to be removed between rounds, ideally at a point where it isn't noticable to players
|
|
SetPlayerDeathsHidden( true ) // hide death sounds and such so people won't notice they're dying
|
|
|
|
foreach ( entity player in GetPlayerArray() )
|
|
{
|
|
ClearTitanAvailable( player )
|
|
PROTO_CleanupTrackedProjectiles( player )
|
|
player.SetPlayerNetInt( "batteryCount", 0 )
|
|
if ( IsAlive( player ) )
|
|
player.Die( svGlobal.worldspawn, svGlobal.worldspawn, { damageSourceId = eDamageSourceId.round_end } )
|
|
}
|
|
|
|
foreach ( entity npc in GetNPCArray() )
|
|
{
|
|
if ( !IsValid( npc ) || !IsAlive( npc ) )
|
|
continue
|
|
// kill rather than destroy, as destroying will cause issues with children which is an issue especially for dropships and titans
|
|
npc.Die( svGlobal.worldspawn, svGlobal.worldspawn, { damageSourceId = eDamageSourceId.round_end } )
|
|
}
|
|
|
|
// destroy weapons
|
|
ClearDroppedWeapons()
|
|
|
|
foreach ( entity battery in GetEntArrayByClass_Expensive( "item_titan_battery" ) )
|
|
battery.Destroy()
|
|
|
|
// allow other scripts to clean stuff up too
|
|
svGlobal.levelEnt.Signal( "CleanUpEntitiesForRoundEnd" )
|
|
foreach ( void functionref() callback in file.roundEndCleanupCallbacks )
|
|
callback()
|
|
|
|
SetPlayerDeathsHidden( false )
|
|
}
|
|
|
|
|
|
|
|
// stuff for gamemodes to call
|
|
|
|
void function SetShouldUsePickLoadoutScreen( bool shouldUse )
|
|
{
|
|
file.usePickLoadoutScreen = shouldUse
|
|
}
|
|
|
|
void function SetSwitchSidesBased( bool switchSides )
|
|
{
|
|
file.switchSidesBased = switchSides
|
|
}
|
|
|
|
void function SetSuddenDeathBased( bool suddenDeathBased )
|
|
{
|
|
file.suddenDeathBased = suddenDeathBased
|
|
}
|
|
|
|
void function SetTimerBased( bool timerBased )
|
|
{
|
|
file.timerBased = timerBased
|
|
}
|
|
|
|
void function SetShouldUseRoundWinningKillReplay( bool shouldUse )
|
|
{
|
|
SetServerVar( "roundWinningKillReplayEnabled", shouldUse )
|
|
}
|
|
|
|
void function SetRoundWinningKillReplayKillClasses( bool pilot, bool titan )
|
|
{
|
|
file.roundWinningKillReplayTrackPilotKills = pilot
|
|
file.roundWinningKillReplayTrackTitanKills = titan // player kills in titans should get tracked anyway, might be worth renaming this
|
|
}
|
|
|
|
void function SetRoundWinningKillReplayAttacker( entity attacker, int inflictorEHandle = -1 )
|
|
{
|
|
file.roundWinningKillReplayTime = Time()
|
|
file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
|
|
file.roundWinningKillReplayAttacker = attacker
|
|
file.roundWinningKillReplayInflictorEHandle = inflictorEHandle == -1 ? attacker.GetEncodedEHandle() : inflictorEHandle
|
|
file.roundWinningKillReplayTimeOfDeath = Time()
|
|
}
|
|
|
|
void function SetWinner( int team, string winningReason = "", string losingReason = "" )
|
|
{
|
|
SetServerVar( "winningTeam", team )
|
|
|
|
file.gameWonThisFrame = true
|
|
thread UpdateGameWonThisFrameNextFrame()
|
|
|
|
if ( winningReason.len() == 0 )
|
|
file.announceRoundWinnerWinningSubstr = 0
|
|
else
|
|
file.announceRoundWinnerWinningSubstr = GetStringID( winningReason )
|
|
|
|
if ( losingReason.len() == 0 )
|
|
file.announceRoundWinnerLosingSubstr = 0
|
|
else
|
|
file.announceRoundWinnerLosingSubstr = GetStringID( losingReason )
|
|
|
|
if ( GamePlayingOrSuddenDeath() )
|
|
{
|
|
if ( IsRoundBased() )
|
|
{
|
|
if ( team != TEAM_UNASSIGNED )
|
|
{
|
|
GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + 1 )
|
|
GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + 1 )
|
|
}
|
|
|
|
SetGameState( eGameState.WinnerDetermined )
|
|
}
|
|
else
|
|
SetGameState( eGameState.WinnerDetermined )
|
|
|
|
ScoreEvent_MatchComplete( team )
|
|
}
|
|
}
|
|
|
|
void function UpdateGameWonThisFrameNextFrame()
|
|
{
|
|
WaitFrame()
|
|
file.gameWonThisFrame = false
|
|
file.hasKillForGameWonThisFrame = false
|
|
}
|
|
|
|
void function AddTeamScore( int team, int amount )
|
|
{
|
|
GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + amount )
|
|
GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + amount )
|
|
|
|
int scoreLimit
|
|
if ( IsRoundBased() )
|
|
scoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE )
|
|
else
|
|
scoreLimit = GameMode_GetScoreLimit( GAMETYPE )
|
|
|
|
int score = GameRules_GetTeamScore( team )
|
|
if ( score >= scoreLimit || GetGameState() == eGameState.SuddenDeath )
|
|
SetWinner( team )
|
|
else if ( ( file.switchSidesBased && !file.hasSwitchedSides ) && score >= ( scoreLimit.tofloat() / 2.0 ) )
|
|
SetGameState( eGameState.SwitchingSides )
|
|
}
|
|
|
|
void function SetTimeoutWinnerDecisionFunc( int functionref() callback )
|
|
{
|
|
file.timeoutWinnerDecisionFunc = callback
|
|
}
|
|
|
|
int function GetWinningTeamWithFFASupport()
|
|
{
|
|
if ( !IsFFAGame() )
|
|
return GameScore_GetWinningTeam()
|
|
else
|
|
{
|
|
// custom logic for calculating ffa winner as GameScore_GetWinningTeam doesn't handle this
|
|
int winningTeam = TEAM_UNASSIGNED
|
|
int winningScore = 0
|
|
|
|
foreach ( entity player in GetPlayerArray() )
|
|
{
|
|
int currentScore = GameRules_GetTeamScore( player.GetTeam() )
|
|
|
|
if ( currentScore == winningScore )
|
|
winningTeam = TEAM_UNASSIGNED // if 2 teams are equal, return TEAM_UNASSIGNED
|
|
else if ( currentScore > winningScore )
|
|
{
|
|
winningTeam = player.GetTeam()
|
|
winningScore = currentScore
|
|
}
|
|
}
|
|
|
|
return winningTeam
|
|
}
|
|
|
|
unreachable
|
|
}
|
|
|
|
// idk
|
|
|
|
float function GameState_GetTimeLimitOverride()
|
|
{
|
|
return 100
|
|
}
|
|
|
|
bool function IsRoundBasedGameOver()
|
|
{
|
|
return false
|
|
}
|
|
|
|
bool function ShouldRunEvac()
|
|
{
|
|
return true
|
|
}
|
|
|
|
void function GiveTitanToPlayer( entity player )
|
|
{
|
|
|
|
}
|
|
|
|
float function GetTimeLimit_ForGameMode()
|
|
{
|
|
string mode = GameRules_GetGameMode()
|
|
string playlistString = "timelimit"
|
|
|
|
// default to 10 mins, because that seems reasonable
|
|
return GetCurrentPlaylistVarFloat( playlistString, 10 )
|
|
}
|
|
|
|
// faction dialogue
|
|
|
|
void function DialoguePlayNormal()
|
|
{
|
|
int totalScore = GameMode_GetScoreLimit( GameRules_GetGameMode() )
|
|
int winningTeam
|
|
int losingTeam
|
|
float diagIntervel = 71 // play a faction dailogue every 70 + 1s to prevent play together with winner dialogue
|
|
|
|
while( GetGameState() == eGameState.Playing )
|
|
{
|
|
wait diagIntervel
|
|
if( GameRules_GetTeamScore( TEAM_MILITIA ) < GameRules_GetTeamScore( TEAM_IMC ) )
|
|
{
|
|
winningTeam = TEAM_IMC
|
|
losingTeam = TEAM_MILITIA
|
|
}
|
|
if( GameRules_GetTeamScore( TEAM_MILITIA ) > GameRules_GetTeamScore( TEAM_IMC ) )
|
|
{
|
|
winningTeam = TEAM_MILITIA
|
|
losingTeam = TEAM_IMC
|
|
}
|
|
if( GameRules_GetTeamScore( winningTeam ) - GameRules_GetTeamScore( losingTeam ) >= totalScore * 0.4 )
|
|
{
|
|
PlayFactionDialogueToTeam( "scoring_winningLarge", winningTeam )
|
|
PlayFactionDialogueToTeam( "scoring_losingLarge", losingTeam )
|
|
}
|
|
else if( GameRules_GetTeamScore( winningTeam ) - GameRules_GetTeamScore( losingTeam ) <= totalScore * 0.2 )
|
|
{
|
|
PlayFactionDialogueToTeam( "scoring_winningClose", winningTeam )
|
|
PlayFactionDialogueToTeam( "scoring_losingClose", losingTeam )
|
|
}
|
|
else if( GameRules_GetTeamScore( winningTeam ) == GameRules_GetTeamScore( losingTeam ) )
|
|
{
|
|
continue
|
|
}
|
|
else
|
|
{
|
|
PlayFactionDialogueToTeam( "scoring_winning", winningTeam )
|
|
PlayFactionDialogueToTeam( "scoring_losing", losingTeam )
|
|
}
|
|
}
|
|
}
|
|
|
|
void function DialoguePlayWinnerDetermined()
|
|
{
|
|
int totalScore = GameMode_GetScoreLimit( GameRules_GetGameMode() )
|
|
int winningTeam
|
|
int losingTeam
|
|
|
|
if( GameRules_GetTeamScore( TEAM_MILITIA ) < GameRules_GetTeamScore( TEAM_IMC ) )
|
|
{
|
|
winningTeam = TEAM_IMC
|
|
losingTeam = TEAM_MILITIA
|
|
}
|
|
if( GameRules_GetTeamScore( TEAM_MILITIA ) > GameRules_GetTeamScore( TEAM_IMC ) )
|
|
{
|
|
winningTeam = TEAM_MILITIA
|
|
losingTeam = TEAM_IMC
|
|
}
|
|
if( IsRoundBased() ) // check for round based modes
|
|
{
|
|
if( GameRules_GetTeamScore( winningTeam ) != GameMode_GetRoundScoreLimit( GAMETYPE ) ) // no winner dialogue till game really ends
|
|
return
|
|
}
|
|
if( GameRules_GetTeamScore( winningTeam ) - GameRules_GetTeamScore( losingTeam ) >= totalScore * 0.4 )
|
|
{
|
|
PlayFactionDialogueToTeam( "scoring_wonMercy", winningTeam )
|
|
PlayFactionDialogueToTeam( "scoring_lostMercy", losingTeam )
|
|
}
|
|
else if( GameRules_GetTeamScore( winningTeam ) - GameRules_GetTeamScore( losingTeam ) <= totalScore * 0.2 )
|
|
{
|
|
PlayFactionDialogueToTeam( "scoring_wonClose", winningTeam )
|
|
PlayFactionDialogueToTeam( "scoring_lostClose", losingTeam )
|
|
}
|
|
else if( GameRules_GetTeamScore( winningTeam ) == GameRules_GetTeamScore( losingTeam ) )
|
|
{
|
|
PlayFactionDialogueToTeam( "scoring_tied", winningTeam )
|
|
PlayFactionDialogueToTeam( "scoring_tied", losingTeam )
|
|
}
|
|
else
|
|
{
|
|
PlayFactionDialogueToTeam( "scoring_won", winningTeam )
|
|
PlayFactionDialogueToTeam( "scoring_lost", losingTeam )
|
|
}
|
|
}
|
|
|
|
/// This is to move all NPCs that a player owns from one team to the other during a match
|
|
/// Auto-Titans, Turrets, Ticks and Hacked Spectres will all move along together with the player to the new Team
|
|
/// Also possibly prevents mods that spawns other types of NPCs that players can own from breaking when switching (i.e Drones, Hacked Reapers)
|
|
void function OnPlayerChangedTeam( entity player )
|
|
{
|
|
if ( !player.hasConnected ) // Prevents players who just joined to trigger below code, as server always pre setups their teams
|
|
return
|
|
|
|
NotifyClientsOfTeamChange( player, GetOtherTeam( player.GetTeam() ), player.GetTeam() )
|
|
|
|
foreach( npc in GetNPCArray() )
|
|
{
|
|
entity bossPlayer = npc.GetBossPlayer()
|
|
if ( IsValidPlayer( bossPlayer ) && bossPlayer == player && IsAlive( npc ) )
|
|
SetTeam( npc, player.GetTeam() )
|
|
}
|
|
}
|