aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut2179
1 files changed, 2179 insertions, 0 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut
new file mode 100644
index 000000000..a4c6e187b
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut
@@ -0,0 +1,2179 @@
+untyped
+
+globalize_all_functions
+
+//********************************************************************************************
+// Base Gametype
+//********************************************************************************************
+const DEATH_CHAT_DELAY = 0.3
+
+global struct OutOfBoundsDataStruct //Have to globalize it because all functions are globalized in this file :/
+{
+ int outOfBoundsTriggersTouched = 0
+ float timeBackInBound = 0
+ float timeLeftBeforeDyingFromOutOfBounds = OUT_OF_BOUNDS_TIME_LIMIT
+}
+
+struct
+{
+ PilotLoadoutDef& playbackBotLoadout
+ array<entity> outOfBoundsTriggers = []
+ array<entity> hurtTriggers = []
+ bool functionref( entity, entity, var ) isProtectedFromFriendlyFire
+ table< entity, OutOfBoundsDataStruct > outOfBoundsTable
+} file
+
+function BaseGametype_Init()
+{
+ FlagInit( "APlayerHasSpawned" )
+ FlagInit( "PilotBot" )
+
+ if ( !reloadingScripts )
+ {
+ level.gameTypeText <- null
+ level.classTypeText <- null
+
+ level.titanAlwaysAvailableForTeam <- [ 0, 0, 0, 0 ]
+
+ level.missingPlayersTimeout <- null
+
+ CreateTeamColorControlPoints()
+
+ AddClientCommandCallback( "CC_SelectRespawn", ClientCommand_SelectRespawn )
+ AddClientCommandCallback( "CC_RespawnPlayer", ClientCommand_RespawnPlayer )
+
+ AddCallback_NPCLeeched( OnNPCLeeched )
+
+ MarkTeamsAsBalanced_Off()
+ }
+
+ if ( IsSingleplayer() )
+ {
+ file.isProtectedFromFriendlyFire = IsProtectedFromFriendlyFire_SP
+ }
+ else
+ {
+ file.isProtectedFromFriendlyFire = IsProtectedFromFriendlyFire_MP
+ }
+
+ RegisterSignal( "OnDamageNotify" )
+ RegisterSignal( "OnRespawned" )
+ RegisterSignal( "ChoseToSpawnAsTitan" )
+ RegisterSignal( "OutOfBounds" )
+ RegisterSignal( "BackInBounds" )
+ RegisterSignal( "PlayerKilled" )
+ RegisterSignal( "RespawnMe" )
+ RegisterSignal( "SimulateGameScore" )
+ RegisterSignal( "ObserverThread" )
+ RegisterSignal( "CE_FLAGS_CHANGED" )
+
+ RegisterSignal( "Stop_OnStartTouch_EntityOutOfBounds" )
+ RegisterSignal( "Stop_OnEndTouch_EntityBackInBounds" )
+
+ RegisterSignal( "OnRespawnSelect" )
+
+ AddCallback_EntitiesDidLoad( BaseGametypeEntitiesDidLoad )
+
+ BaseGametype_Init_MPSP()
+
+ AddCallback_OnTitanBecomesPilot( OnTitanBecomesPilot_OutOfBoundsCheck )
+}
+
+void function BaseGametypeEntitiesDidLoad()
+{
+ OutOfBoundsSetup()
+ TriggerHurtSetup()
+}
+
+function CreateTeamColorControlPoints()
+{
+ Assert( !( "fx_CP_color_enemy" in level ) )
+ Assert( !( "fx_CP_color_friendly" in level ) )
+
+ entity enemy = CreateEntity( "info_placement_helper" )
+ SetTargetName( enemy, UniqueString( "teamColorControlPoint_enemy" ) )
+ enemy.kv.start_active = 1
+ DispatchSpawn( enemy )
+
+ enemy.SetOrigin( ENEMY_COLOR_FX )
+ svGlobal.fx_CP_color_enemy = enemy
+
+ entity friendly = CreateEntity( "info_placement_helper" )
+ SetTargetName( friendly, UniqueString( "teamColorControlPoint_friendly" ) )
+ friendly.kv.start_active = 1
+ DispatchSpawn( friendly )
+
+ friendly.SetOrigin( FRIENDLY_COLOR_FX )
+ svGlobal.fx_CP_color_friendly = friendly
+
+ entity neutral = CreateEntity( "info_placement_helper" )
+ SetTargetName( neutral, UniqueString( "teamColorControlPoint_neutral" ) )
+ neutral.kv.start_active = 1
+ DispatchSpawn( neutral )
+
+ neutral.SetOrigin( NEUTRAL_COLOR_FX )
+ svGlobal.fx_CP_color_neutral = neutral
+}
+
+const SOLDIER_SOUND_PAIN = "npc_grunt_pain"
+
+void function CodeCallback_OnPrecache()
+{
+ if ( IsLobby() )
+ return
+
+ Assert( IsSingleplayer() || GAMETYPE in GAMETYPE_TEXT )
+
+ // these should be level specific in SP
+ PrecacheEntity( "npc_soldier" )
+ PrecacheEntity( "turret" )
+
+ PrecacheEntity( "npc_dropship", DROPSHIP_MODEL )
+
+ //Scavenger ore models. Need to precache here instead of in gamemode scripts for vpk builds
+ //Removing for build
+ /*level.scavengerSmallRocks <- [
+ $"models/rocks/rock_01_sandstone.mdl"
+ //$"models/rocks/rock_02_sandstone.mdl"
+ //$"models/rocks/rock_03_sandstone.mdl"
+ //$"models/rocks/single_rock_01.mdl"
+ //$"models/rocks/single_rock_02.mdl"
+ //$"models/rocks/single_rock_03.mdl"
+ //$"models/rocks/single_rock_04.mdl"
+ ]
+
+ level.scavengerLargeRocks <- [
+ $"models/rocks/rock_boulder_large_01.mdl"
+ //$"models/rocks/sandstone_rock01.mdl"
+ //$"models/rocks/sandstone_rock02.mdl"
+ //$"models/rocks/sandstone_rock03.mdl"
+ //$"models/rocks/sandstone_rock04.mdl"
+ //$"models/rocks/sandstone_rock05.mdl"
+ ]
+
+ foreach ( model in level.scavengerSmallRocks )
+ {
+ PrecacheModel( model )
+ }
+
+ foreach ( model in level.scavengerLargeRocks )
+ {
+ PrecacheModel( model )
+ }*/
+
+ if ( !IsMenuLevel() )
+ {
+ InitGameState()
+ SetGameState( eGameState.WaitingForPlayers )
+ }
+
+ level.ui.disableDev = IsMatchmakingServer()
+}
+
+function AddFlinch( entity attackedEnt, damageInfo )
+{
+ Assert( IsValid_ThisFrame( attackedEnt ) )
+
+ //if ( !( "nextFlinchTime" in attackedEnt.s ) )
+ // attackedEnt.s.nextFlinchTime <- 0
+ //if ( Time() < attackedEnt.s.nextFlinchTime )
+ // return
+ //attackedEnt.s.nextFlinchTime = Time() + RandomFloatRange( 2.0, 4.0 )
+
+ vector damageAngles = VectorToAngles( DamageInfo_GetDamageForce( damageInfo ) )
+ vector entAngles = attackedEnt.EyeAngles()
+
+ float damageYaw = (damageAngles.y + 180) - entAngles.y
+
+ damageYaw = AngleNormalize( damageYaw )
+
+ if ( damageYaw < 0 )
+ damageYaw += 360
+
+ if ( damageYaw < 45 )
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_BACKWARDS );
+ else if ( damageYaw < 135 )
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_RIGHT );
+ else if ( damageYaw < 225 )
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_FORWARDS );
+ else if ( damageYaw < 315 )
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_LEFT );
+ else
+ DamageInfo_SetFlinchDirection( damageInfo, FLINCH_DIRECTION_BACKWARDS );
+}
+
+
+bool function IsProtectedFromFriendlyFire_MP( entity attacker, entity ent, var damageInfo )
+{
+ // no suicide protection
+ if ( attacker == ent )
+ return false
+
+ if ( attacker.GetTeam() != ent.GetTeam() )
+ return false
+
+ if ( DamageIgnoresFriendlyFire( damageInfo ) )
+ return false
+
+ if ( ent.GetOwner() != attacker && ent.GetBossPlayer() != attacker )
+ return true
+
+ if ( ent.e.noOwnerFriendlyFire == true )
+ return true
+
+ if ( ent.IsNPC() && ent.ai.preventOwnerDamage )
+ return true
+
+ return false
+}
+
+bool function IsProtectedFromNPCFire( entity attacker, entity ent )
+{
+ if ( attacker == ent )
+ return false
+ if ( attacker.IsNPC() && ent.IsNPC() && ent.ai.invulnerableToNPC == true )
+ return true
+ return false
+}
+
+
+bool function IsProtectedFromFriendlyFire_SP( entity attacker, entity ent, var damageInfo )
+{
+ // no suicide protection
+ if ( attacker == ent )
+ return false
+
+ if ( attacker.GetTeam() == ent.GetTeam() )
+ {
+ if ( attacker.IsNPC() )
+ {
+ // dont titanfall me!
+ if ( ent.IsPlayer() )
+ return true
+
+ // bullets dont damage same team of npcs
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_BULLET )
+ return true
+ }
+ else if ( attacker.IsPlayer() )
+ {
+ if ( ent.IsNPC() )
+ {
+ if ( ent.IsTitan() )
+ return true
+
+ return !ent.AISetting_ShootableByFriendlyPlayer()
+ }
+ if ( ent.IsProjectile() )
+ return false
+ return true
+ }
+
+ if ( DamageIgnoresFriendlyFire( damageInfo ) )
+ return false
+
+ if ( ent.IsNPC() && ent.ai.preventOwnerDamage )
+ {
+ if ( attacker == ent.GetOwner() || attacker == ent.GetBossPlayer() )
+ return true
+ }
+ }
+
+ return false
+}
+
+bool function DamageIgnoresFriendlyFire( damageInfo )
+{
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return true
+
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ switch ( damageSourceID )
+ {
+ case eDamageSourceId.switchback_trap:
+ case eDamageSourceId.suicideSpectreAoE:
+ case eDamageSourceId.mp_titanweapon_stun_laser: // for energy transfer functionality. Preventing FF damage in the callback.
+ case eDamageSourceId.mp_titanability_smoke: // For FD Vanguard Shield Upgrades. Preventing FF damage in the callback.
+ return true
+ }
+
+ return false
+}
+
+bool function ScriptCallback_ShouldEntTakeDamage( entity ent, damageInfo )
+{
+ if ( ent.IsInvulnerable() )
+ return false
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ bool entIsPlayer = ent.IsPlayer()
+
+ if ( !attacker )
+ return false
+
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ if ( attacker == ent || IsValid( inflictor ) && inflictor == ent )
+ {
+ if ( (damageType & DF_NO_SELF_DAMAGE) > 0 )
+ return false
+ }
+
+ if ( file.isProtectedFromFriendlyFire( attacker, ent, damageInfo ) )
+ return false
+
+ if ( IsProtectedFromNPCFire( attacker, ent ) )
+ return false
+
+ if ( !ShouldEntTakeDamage_SPMP( ent, damageInfo ) )
+ return false
+
+ if ( ent.IsTitan() )
+ {
+ const int BULLET_VORTEX_FLAGS = (DF_VORTEX_REFIRE | DF_BULLET)
+ if ( ((damageType & BULLET_VORTEX_FLAGS) == BULLET_VORTEX_FLAGS) && (ent == attacker) )
+ return false // don't let vortex-refiring titan hit themselves with bullet or bullet splash damage
+
+ if ( IsTitanWithinBubbleShield( ent ) && TitanHasBubbleShieldWeapon( ent ) && !(damageType & DF_DOOMED_HEALTH_LOSS) )
+ return false
+ }
+
+ if ( IsTitanCrushDamage( damageInfo ) )
+ {
+ if ( attacker.IsPhaseShifted() )
+ return false
+ }
+
+ if ( (inflictor != null) )
+ {
+ if ( inflictor.IsProjectile() )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( attacker == ent )
+ {
+ bool shouldDamageOwner = inflictor.GetProjectileWeaponSettingBool( eWeaponVar.explosion_damages_owner )
+ if ( !shouldDamageOwner )
+ return false
+
+ if ( entIsPlayer )
+ {
+ array<string> mods = inflictor.ProjectileGetMods()
+ foreach ( mod in mods )
+ {
+ if ( mod == "jump_kit" )
+ {
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ damageAmount *= 0.75
+ DamageInfo_SetDamage( damageInfo, damageAmount )
+ // DamageInfo_SetDamageForce( damageInfo, DamageInfo_GetDamageForce( damageInfo ) * 2.0 )
+ }
+ }
+ }
+ }
+ }
+
+ if ( inflictor.e.onlyDamageEntitiesOnce == true && inflictor.e.damagedEntities.contains( ent ) )
+ return false
+
+ if ( inflictor.e.onlyDamageEntitiesOncePerTick == true )
+ {
+ float currentTime = Time()
+ if ( currentTime != inflictor.e.lastDamageTickTime )
+ {
+ inflictor.e.damagedEntities.clear()
+ inflictor.e.lastDamageTickTime = currentTime
+ }
+ else if ( inflictor.e.damagedEntities.contains( ent ) )
+ {
+ return false
+ }
+ }
+ }
+
+ if ( ent.IsPlayer() )
+ {
+ return ShouldPlayerTakeDamage( ent, damageInfo )
+ }
+
+ return true
+}
+
+bool function ShouldPlayerTakeDamage( entity player, damageInfo )
+{
+ if ( player.IsGodMode() )
+ return false
+
+ if ( player.IsPhaseShifted()
+ && !(DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS)
+ && !IsDamageFromDamageTrigger( damageInfo ) )
+ return false
+
+ if ( player.IsInvulnerable() )
+ return false
+
+ if ( player.IsTitan() )
+ {
+ return true
+ }
+ else
+ {
+ //Rodeo cases
+ entity titanSoul = player.GetTitanSoulBeingRodeoed()
+ if ( IsValid( titanSoul ) )
+ {
+ entity titan = titanSoul.GetTitan()
+ //Stop being stepped on by the guy you are rodeoing
+ if ( IsTitanCrushDamage( damageInfo ) && ( titan == DamageInfo_GetAttacker( damageInfo ) ) )
+ return false
+ else
+ return true
+ }
+ else
+ {
+ return true
+ }
+ }
+
+ unreachable
+}
+
+
+void function HandlePainSounds( entity ent, var damageInfo )
+{
+ //exit if the thing is dead
+ if ( ent.GetHealth() < DamageInfo_GetDamage( damageInfo ) )
+ return
+
+ PlayPainSounds( ent, damageInfo )
+}
+
+float function GetHeadshotDamageMultiplierFromDamageInfo( var damageInfo )
+{
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ if ( weapon )
+ {
+ float result = weapon.GetWeaponSettingFloat( eWeaponVar.damage_headshot_scale )
+ return result
+ }
+
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ if ( inflictor && inflictor.IsProjectile() )
+ {
+ float result = inflictor.GetProjectileWeaponSettingFloat( eWeaponVar.damage_headshot_scale )
+ return result
+ }
+
+ return 1.0
+}
+
+function HandleLocationBasedDamage( entity ent, var damageInfo )
+{
+ // Don't allow non-players to get headshots or any other location bonuses
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsValid( attacker ) || !attacker.IsPlayer() )
+ return
+
+ bool debugPrints = false
+ int hitGroup = DamageInfo_GetHitGroup( damageInfo )
+
+ if ( debugPrints )
+ {
+ printt( "---------------------" )
+ printt( "LOCATION BASED DAMAGE" )
+ printt( "HIDGROUP ID:", hitGroup )
+ if ( hitGroup == HITGROUP_GENERIC )
+ printt( "HITGROUP: HITGROUP_GENERIC" )
+ else if ( hitGroup == HITGROUP_HEAD )
+ printt( "HITGROUP: HITGROUP_HEAD" )
+ else if ( hitGroup == HITGROUP_CHEST )
+ printt( "HITGROUP: HITGROUP_CHEST" )
+ else if ( hitGroup == HITGROUP_STOMACH )
+ printt( "HITGROUP: HITGROUP_STOMACH" )
+ else if ( hitGroup == HITGROUP_LEFTARM )
+ printt( "HITGROUP: HITGROUP_LEFTARM" )
+ else if ( hitGroup == HITGROUP_RIGHTARM )
+ printt( "HITGROUP: HITGROUP_RIGHTARM" )
+ else if ( hitGroup == HITGROUP_LEFTLEG )
+ printt( "HITGROUP: HITGROUP_LEFTLEG" )
+ else if ( hitGroup == HITGROUP_RIGHTLEG )
+ printt( "HITGROUP: HITGROUP_RIGHTLEG" )
+ else if ( hitGroup == HITGROUP_GEAR )
+ printt( "HITGROUP: HITGROUP_GEAR" )
+ else
+ printt( "HITGROUP: UNKNOWN" )
+ }
+
+ bool isValidHeadShot = IsValidHeadShot( damageInfo, ent )
+ if ( isValidHeadShot )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_HEADSHOT )
+
+ float damageMult_location = 1.0
+
+ var weaponName // TODO: If set to type string, will cause errors because weaponName can be ""
+ if ( DamageInfo_GetWeapon( damageInfo ) )
+ weaponName = DamageInfo_GetWeapon( damageInfo ).GetWeaponClassName()
+ else if ( DamageInfo_GetInflictor( damageInfo ) && (DamageInfo_GetInflictor( damageInfo ) instanceof CProjectile ) )
+ weaponName = DamageInfo_GetInflictor( damageInfo ).ProjectileGetWeaponClassName()
+
+ if ( ent.IsTitan() )
+ {
+ damageMult_location = GetCriticalScaler( ent, damageInfo )
+ }
+ else if ( IsSuperSpectre( ent ) )
+ {
+ if ( CritWeaponInDamageInfo( damageInfo ) && IsCriticalHit( attacker, ent, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) ) )
+ {
+ damageMult_location = GetCriticalScaler( ent, damageInfo )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+ }
+ }
+ else if ( IsStalker( ent ) )
+ {
+ // note: stalker location based damage is done in _ai_stalker.gnut.
+ switch ( hitGroup )
+ {
+ case HITGROUP_GEAR:
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+ break
+ }
+ }
+ else if ( isValidHeadShot )
+ {
+ damageMult_location = GetHeadshotDamageMultiplierFromDamageInfo( damageInfo )
+ }
+
+ // modify damage value based on where we hit
+ if ( damageMult_location != 1.0 )
+ {
+ if ( debugPrints )
+ {
+ printt( "Multiplier:", damageMult_location )
+ printt( "---------------------" )
+ }
+
+ DamageInfo_ScaleDamage( damageInfo, damageMult_location )
+ }
+}
+
+function PlayerDamageFeedback( entity ent, damageInfo )
+{
+// printt( "player damage feedback for " + ent )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ Assert( attacker.IsPlayer() )
+
+ int customDamageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ if ( IsMaxRangeShot( damageInfo ) )
+ customDamageType = customDamageType | DF_MAX_RANGE
+
+ if ( ent.GetHealth() - DamageInfo_GetDamage( damageInfo ) <= 0 )
+ {
+ if ( !ent.IsNPC() || ent.ai.killShotSound )
+ customDamageType = customDamageType | DF_KILLSHOT
+ }
+
+ attacker.NotifyDidDamage( ent, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamagePosition( damageInfo ), customDamageType, DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageFlags( damageInfo ), DamageInfo_GetHitGroup( damageInfo ), DamageInfo_GetWeapon( damageInfo ), DamageInfo_GetDistFromAttackOrigin( damageInfo ) )
+}
+
+void function UpdateLastDamageTime( entity ent )
+{
+ if ( !ent.IsPlayer() )
+ return
+
+ ent.p.lastDamageTime = Time()
+}
+
+void function PlayerDealtTitanDamage( entity attacker, entity victim, float savedDamage, var damageInfo )
+{
+ if ( attacker != victim )
+ {
+ attacker.p.titanDamageDealt += savedDamage
+
+#if MP
+ UpdateTitanWeaponDamageStat( attacker, savedDamage, damageInfo )
+
+ if ( attacker.IsTitan() )
+ {
+ attacker.p.titanDamageDealt_Stat += savedDamage
+ if ( attacker.p.titanDamageDealt_Stat >= 500 ) // buffer the titan stat damage so that we don't spam damage callbacks
+ {
+ UpdateTitanDamageStat( attacker, attacker.p.titanDamageDealt_Stat, damageInfo )
+ attacker.p.titanDamageDealt_Stat = 0
+ }
+ }
+#endif
+ }
+}
+
+function UpdateAttackerInfo( entity ent, entity attacker, damage )
+{
+ entity attackerPlayer = GetPlayerFromEntity( attacker )
+ if ( !attackerPlayer )
+ return
+
+ // cannot be your own last attacker
+ if ( attackerPlayer == ent )
+ return
+
+ if ( !damage || damage <= 0 )
+ return
+
+ if ( !("attackerInfo" in ent.s) )
+ ent.s.attackerInfo <- {}
+ else if ( ent.GetHealth() == ent.GetMaxHealth() )
+ ent.s.attackerInfo.clear()
+
+ if ( !(attackerPlayer.weakref() in ent.s.attackerInfo ) )
+ ent.s.attackerInfo[attackerPlayer.weakref()] <- 0
+
+ ent.s.attackerInfo[attackerPlayer.weakref()] += damage
+
+ ent.e.lastAttacker = attackerPlayer
+}
+
+entity function GetAttackerPlayerOrBossPlayer( entity attacker )
+{
+ if ( !IsValid( attacker ) )
+ return null
+
+ if ( attacker.IsPlayer() )
+ return attacker
+
+ entity bossPlayer = attacker.GetBossPlayer()
+ if ( !IsValid( bossPlayer ) )
+ return null
+
+ return bossPlayer
+}
+
+entity function GetAttackerOrLastAttacker( entity ent, damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( ShouldGetLastAttacker( ent, attacker ) == false )
+ return attacker
+
+ entity lastAttacker = GetLastAttacker( ent ) //Attacker doesn't work, get last attacker
+
+ if ( IsValid( lastAttacker ) == true )
+ return lastAttacker
+
+ //last attacker doesn't work, get latestAssistingPlayerInfo
+ AssistingPlayerStruct attackerInfo = GetLatestAssistingPlayerInfo( ent )
+ if ( IsValid( attackerInfo.player ) )
+ return attackerInfo.player
+
+ if ( IsValid( attacker ) ) //No Last Attacker and No Lastest Assisting Player, e.g. when you suicide before taking damage. Just return the attacker if valid
+ return attacker
+
+ return null
+}
+
+bool function ShouldGetLastAttacker( entity ent, entity attacker )
+{
+ if ( IsValid( attacker ) == false )
+ return true
+
+ if ( attacker == ent ) //suicide
+ return true
+
+ if ( attacker.IsPlayer() == false && attacker.IsNPC() == false ) //Environmental damage
+ return true
+
+ return false
+}
+
+function ClearLastAttacker( entity ent )
+{
+ ent.e.lastAttacker = null
+}
+
+entity function GetLastAttacker( entity ent )
+{
+ if ( ent.IsTitan() && IsValid( ent.GetTitanSoul() ) ) // JFS: second check is defensive
+ {
+ entity soul = ent.GetTitanSoul()
+ if ( soul.lastAttackInfo && "attacker" in soul.lastAttackInfo && IsValid( soul.lastAttackInfo.attacker ) )
+ return expect entity( soul.lastAttackInfo.attacker )
+ }
+
+ if ( !IsValid( ent.e.lastAttacker ) )
+ return null
+
+ return ent.e.lastAttacker
+}
+
+bool function PlayerOrNPCKilled( entity ent, var damageInfo )
+{
+ bool gamePlayingOrSuddenDeath = GamePlayingOrSuddenDeath() // Storing this off here, the game state can change in the callbacks below which may cause kills to not count
+
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ if ( damageSourceID == eDamageSourceId.round_end )
+ return false
+
+ entity attacker = GetAttackerOrLastAttacker( ent, damageInfo )
+ if ( !IsValid( attacker ) )
+ return false
+
+ if ( ent.IsPlayer() )
+ {
+ LogPlayerMatchStat_Death( ent )
+
+ if ( attacker.IsPlayer() && (attacker != ent) )
+ LogPlayerMatchStat_KilledAPilot( attacker )
+ }
+
+ if ( ent.IsNPC() && !IsValidNPCTarget( ent ) )
+ return false
+
+ if ( !attacker.IsPlayer() )
+ {
+ entity newAttacker = GetPlayerFromEntity( attacker )
+ if ( IsValid( newAttacker ) )
+ attacker = newAttacker
+ }
+
+ if ( ent.IsPlayer() )
+ {
+ //Do callbacks. Main reason we call this here as opposed to CodeCallback_OnPlayerKilled() is legacy script compatibility reasons.
+ //For example: In script immediately above this we change the attacker to get the player behind the kill, e.g. owner of a pet titan, etc. Bunch of registered callbacks depends on this.
+ foreach( callbackFunc in svGlobal.onPlayerKilledCallbacks )
+ callbackFunc( ent, attacker, damageInfo )
+ }
+ else if ( ent.IsNPC() )
+ {
+ //Do callbacks. Main reason we call this here as opposed to CodeCallback_OnNPCKilled() is legacy script compatibility reasons.
+ //For example: In script immediately above this we change the attacker to get the player behind the kill, e.g. owner of a pet titan, etc. Bunch of registered callbacks depends on this.
+ foreach( callbackFunc in svGlobal.onNPCKilledCallbacks )
+ {
+ callbackFunc( ent, attacker, damageInfo )
+ }
+ }
+
+ if ( ent.IsTitan() )
+ {
+ thread TitanVO_DelayedTitanDown( ent )
+ }
+
+ if ( !attacker.IsPlayer() )
+ {
+ // This gets the last player that did damage to the entity so that we can give him the kill
+ AssistingPlayerStruct attackerInfo = GetLatestAssistingPlayerInfo( ent )
+ attacker = attackerInfo.player
+
+ if ( !IsValid( attacker ) )
+ return true
+
+ // Hack - attacker history isn't on client to calculate if a player should get credit for a kill when AI steals the final killing shot while a player is damaging them.
+ array<entity> playerArray = GetPlayerArray()
+ foreach ( player in playerArray )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_SetAssistInformation", attackerInfo.damageSourceId, attacker.GetEncodedEHandle(), ent.GetEncodedEHandle(), attackerInfo.assistTime )
+ }
+ }
+
+ // player attacker only from here down
+
+ PreScoreEventUpdateStats( attacker, ent )
+ if ( ent.GetTeam() != attacker.GetTeam() )
+ {
+ if ( ent.IsPlayer() )
+ ScoreEvent_PlayerKilled( ent, attacker, damageInfo )
+ else if ( ent.IsTitan() && ent.IsNPC() )
+ ScoreEvent_TitanKilled( ent, attacker, damageInfo )
+ else
+ ScoreEvent_NPCKilled( ent, attacker, damageInfo )
+ }
+ PostScoreEventUpdateStats( attacker, ent )
+
+ if ( ent.GetTeam() == attacker.GetTeam() )
+ {
+ return false
+ }
+
+ // Respawn Kill INFECTION!!
+ if ( ent.IsPlayer() && attacker.IsPlayer() )
+ {
+ if ( ent.GetPersistentVar( "respawnKillInfected" ) && !attacker.GetPersistentVar( "respawnKillInfected" ) )
+ attacker.SetPersistentVar( "respawnKillInfected", true )
+ }
+
+ if ( gamePlayingOrSuddenDeath )
+ {
+ if ( ent.IsPlayer() )
+ {
+ if ( ent.IsTitan() )
+ {
+ //if we killed a player in a titan count two kills (one for the pilot, one for the titan )
+ attacker.AddToPlayerGameStat( PGS_KILLS, 2 )
+ attacker.AddToPlayerGameStat( PGS_TITAN_KILLS, 1 )
+ attacker.AddToPlayerGameStat( PGS_PILOT_KILLS, 1 )
+ }
+ else
+ {
+ attacker.AddToPlayerGameStat( PGS_KILLS, 1 )
+ attacker.AddToPlayerGameStat( PGS_PILOT_KILLS, 1 )
+ }
+ }
+ else
+ {
+ if ( ent.IsTitan() )
+ attacker.AddToPlayerGameStat( PGS_TITAN_KILLS, 1 )
+
+ if( !IsMarvin( ent ) && !ent.IsTitan() )
+ attacker.AddToPlayerGameStat( PGS_NPC_KILLS, 1 )
+ }
+ }
+
+ return true
+}
+
+// used to calculate build time credit in special cases. Cloak Drones and Suicide Spectres use it for now.
+float function CalculateBuildTimeCredit( entity attacker, entity target, float damage, int health, int maxHealth, string playlistVarStr, float defaultCredit )
+{
+ float titanSpawnDelay = GetTitanBuildTime( attacker )
+ float timerCredit = 0
+
+ health = maxint( 0, health ) // health should never be less then 0
+ if ( titanSpawnDelay && IsAlive( target ) )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( playlistVarStr, defaultCredit )
+
+ float dealtDamage = min( health, damage )
+ timerCredit = timerCredit * (dealtDamage / maxHealth )
+ }
+
+ return timerCredit
+}
+
+function UpdateNextRespawnTime( entity player, float time )
+{
+ player.nv.nextRespawnTime = time
+}
+
+bool function ShouldSetObserverTarget( entity attacker )
+{
+ if ( !IsAlive( attacker ) )
+ return false
+
+ if ( attacker.IsPlayer() && attacker.IsObserver() )
+ return false
+
+ return true
+}
+
+float function CalculateLengthOfKillReplay( entity player, int methodOfDeath ) //Meant to be called on the same frame player dies
+{
+ return GetDeathCamLength( player ) + GetKillReplayBeforeTime( player, methodOfDeath ) + GetKillReplayAfterTime( player )
+}
+
+float function GetKillReplayBeforeTime( entity player, int methodOfDeath )
+{
+ switch ( methodOfDeath )
+ {
+ case eDamageSourceId.damagedef_titan_fall:
+ case eDamageSourceId.damagedef_titan_hotdrop:
+ case eDamageSourceId.damagedef_reaper_fall:
+ case eDamageSourceId.droppod_impact:
+ return KILL_REPLAY_BEFORE_KILL_TIME_DROPPOD
+ }
+
+ if ( !GamePlayingOrSuddenDeath() )
+ return KILL_REPLAY_BEFORE_KILL_TIME_SHORT
+
+ float titanKillReplayTime = KILL_REPLAY_BEFORE_KILL_TIME_TITAN
+ float pilotKillReplayTime = KILL_REPLAY_BEFORE_KILL_TIME_PILOT
+ switch ( methodOfDeath )
+ {
+ case eDamageSourceId.titan_execution:
+ return titanKillReplayTime + 3.0
+
+ case eDamageSourceId.switchback_trap:
+ if ( player.IsTitan() )
+ return titanKillReplayTime + 6.0
+ else
+ return pilotKillReplayTime + 8.0
+ }
+
+ if ( player.IsTitan() )
+ return titanKillReplayTime
+
+ // titan recently?
+ if ( Time() - player.lastTitanTime < 5.0 )
+ return titanKillReplayTime
+
+ return pilotKillReplayTime
+}
+
+function TrackDestroyTimeForReplay( entity attacker, table replayTracker )
+{
+ float startTime = Time()
+ // tracks the time until the attacker becomes invalid
+ EndSignal( replayTracker, "OnDestroy" )
+
+ OnThreadEnd(
+ function () : ( replayTracker, startTime )
+ {
+ replayTracker.validTime = Time() - startTime
+ }
+ )
+
+ string signal = "OnDestroy"
+
+ if ( IsAlive( attacker ) )
+ attacker.WaitSignal( signal )
+ else
+ WaitSignalOnDeadEnt( attacker, signal )
+}
+
+#if MP
+function PlayerWatchesKillReplay( entity player, int inflictorEHandle, int attackerViewIndex, float timeSinceAttackerSpawned, float timeOfDeath, float beforeTime, table replayTracker )
+{
+ OnThreadEnd(
+ function () : ( player, replayTracker )
+ {
+ Signal( replayTracker, "OnDestroy" )
+ }
+ )
+
+ player.EndSignal( "RespawnMe" )
+
+ float timeBeforeKill = beforeTime
+ float timeAfterKill = GetKillReplayAfterTime( player )
+
+ if ( timeBeforeKill > timeSinceAttackerSpawned )
+ timeBeforeKill = timeSinceAttackerSpawned
+
+ float replayDelay = timeBeforeKill + ( Time() - timeOfDeath )
+ if ( replayDelay < 0 )
+ {
+ print( "PlayerWatchesKillReplay(): replayDelay is < 0 (" + replayDelay + "). Aborting kill replay.\n" )
+ return
+ }
+
+ player.SetKillReplayDelay( replayDelay, THIRD_PERSON_KILL_REPLAY_ALWAYS )
+ player.SetKillReplayInflictorEHandle( inflictorEHandle )
+ player.SetKillReplayVictim( player )
+ player.SetViewIndex( attackerViewIndex )
+
+ wait timeBeforeKill
+
+ if ( replayTracker.validTime != null && replayTracker.validTime < timeAfterKill )
+ {
+ float waitTime = expect float( replayTracker.validTime ) - 0.1 // cut off just before ent becomes invalid in the past
+ if ( waitTime > 0 )
+ wait waitTime
+ }
+ else
+ {
+ wait timeAfterKill
+ }
+}
+#endif // #if MP
+
+bool function ClientCommand_SelectRespawn( entity player, array<string> args )
+{
+ if ( IsAlive( player ) )
+ return true
+
+ if ( args.len() == 0 )
+ return true
+
+ int index = args[ 0 ].tointeger()
+
+ switch ( index )
+ {
+ case 1:
+ player.SetPersistentVar( "spawnAsTitan", true )
+ break
+ case 2:
+ player.SetPersistentVar( "spawnAsTitan", false )
+ break
+ }
+
+ return true
+}
+
+
+bool function ClientCommand_RespawnPlayer( entity player, array<string>args )
+{
+ if ( IsSingleplayer() )
+ return true
+
+ if ( IsAlive( player ) )
+ return true
+
+ if ( args.len() != 1 )
+ return true
+
+ string opParm = args[ 0 ]
+
+ if ( opParm.find( "burncard" ) != null )
+ {
+ //int burnCard = opParm.tointeger()
+ //SetPlayerBurnCardSlotToActivate( player, burnCard )
+ return true
+ }
+ else if ( opParm == "Titan" )
+ {
+ player.SetPersistentVar( "spawnAsTitan", true )
+ }
+ else if ( opParm == "Pilot" )
+ {
+ player.SetPersistentVar( "spawnAsTitan", false )
+ }
+
+ float deathCamLength = GetDeathCamLength( player )
+ float skipBufferTime = 0.5
+ if ( Time() > (player.p.postDeathThreadStartTime + deathCamLength) - skipBufferTime )
+ {
+ player.s.respawnSelectionDone = true
+ player.Signal( "RespawnMe" )
+ }
+
+ return true
+}
+
+function AIChatter( string alias, int team, vector origin )
+{
+ array<entity> ai = GetNearbyFriendlyGrunts( origin, team )
+
+ if ( ai.len() > 0 )
+ {
+ PlaySquadConversationToAll( alias, ai[0] )
+ }
+}
+
+const MAX_ACTIVITY_DISABLED = 0
+const MAX_ACTIVITY_PILOTS = 1
+const MAX_ACTIVITY_TITANS = 2
+const MAX_ACTIVITY_PILOTS_AND_TITANS = 3
+const MAX_ACTIVITY_CONGER_MODE = 4
+
+bool function GetPilotBotFlag()
+{
+ // IMPORTANT: Please call this consistently instead of Flag( "PilotBot" )
+ // Force titan or pilot bots according to max activity mode if it is enabled.
+ // Otherwise, leave the "pilotBot" flag alone and do what the game mode wants.
+ int max_activity_mode = GetConVarInt( "max_activity_mode" )
+ if ( max_activity_mode == MAX_ACTIVITY_PILOTS || max_activity_mode == MAX_ACTIVITY_PILOTS_AND_TITANS )
+ return true
+ else if ( max_activity_mode == MAX_ACTIVITY_TITANS )
+ return false
+ else if ( max_activity_mode == MAX_ACTIVITY_CONGER_MODE )
+ return rand() % 2 != 0 // conger mode: 50/50 pilot and titan bots!
+ else
+ return Flag( "PilotBot" )
+
+ unreachable
+}
+
+
+function DoRespawnPlayer( entity player, entity spawnPoint )
+{
+ player.p.lastSpawnPoint = spawnPoint
+ player.RespawnPlayer( spawnPoint ) //This will send "OnRespawned" signal, killing the thread if started from PostDeathThread
+}
+
+function SetupPilotSpawnOnRematch( entity player )
+{
+ // clear respawn countdown message
+ if ( GetWaveSpawnType() == eWaveSpawnType.DROPSHIP )
+ MessageToPlayer( player, eEventNotifications.Clear )
+
+ player.SetOrigin( player.p.rematchOrigin )
+
+ if ( GetWaveSpawnType() == eWaveSpawnType.DISABLED )
+ wait 0.9
+
+ if ( IsAlive( player ) )//HACK: This seems terrible, we shouldn't have to do this
+ {
+ printt( "This happened one time, in retail." )
+ return
+ }
+
+ if ( ShouldGivePlayerInfoOnSpawn() )
+ thread GivePlayerInfoOnSpawn( player )
+
+ return
+}
+
+bool function ShouldGivePlayerInfoOnSpawn()
+{
+ return GetCurrentPlaylistVarInt( "minimap_sonar_pulse_on_respawn", 0 ) > 0
+}
+
+function GivePlayerInfoOnSpawn( entity player )
+{
+ player.EndSignal( "OnDeath" )
+
+ //PrintFunc()
+
+ while( player.IsWatchingKillReplay() )
+ WaitFrame()
+
+ //printt( " GivePlayerInfoOnSpawn Player isn't watching kill replay anymore!" )
+
+ wait 0.2 //Hack: Have to wait even though player should not be watching kill replay anymore...
+
+ //This needed a wait, probably because at this time we haven't given them loadouts yet, so when we do give them loadouts it strips out the passive?
+ thread ScanMinimap( player, true, 0.5 ) //x second minimap pulse
+}
+
+bool function ShouldStartSpawn( entity player )
+{
+ if ( Riff_FloorIsLava() )
+ return false
+
+ if ( Flag( "ForceStartSpawn" ) )
+ return true
+
+ if ( Flag( "IgnoreStartSpawn" ) )
+ return false
+
+ if ( GetGameState() <= eGameState.Prematch )
+ return true
+
+ if ( player.s.respawnCount )
+ return false
+
+ return GameTime_PlayingTime() < START_SPAWN_GRACE_PERIOD
+}
+
+void function PlayerSpawnsIntoPetTitan( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ entity titan = player.GetPetTitan()
+
+ vector origin = titan.GetOrigin() + Vector( 0, 0, 600 )
+ vector angles = titan.GetAngles()
+
+ entity camera = CreateTitanDropCamera( origin, Vector(90,angles.y,0) )
+ player.SetViewEntity( camera, false )
+
+ player.isSpawning = true // set this to prevent .isSpawning checks from returning false
+
+ angles.x = 70
+
+ player.SetOrigin( origin )
+ player.SnapEyeAngles( angles )
+ player.SetVelocity( Vector( 0.0, 0.0, 0.0 ) )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ {
+ player.ClearViewEntity()
+ player.ClearSpawnPoint()
+ player.isSpawning = null
+ }
+ }
+ )
+
+ wait 0.2
+
+ local criteria = {
+ embark = "above_close",
+ titanCanStandRequired = true
+ }
+
+ local embarkAction
+ embarkAction = FindEmbarkActionForCriteria( criteria )
+ if ( embarkAction == null )
+ embarkAction = GetRandomEmbarkAction()
+
+ if ( IsValid( camera ) )
+ {
+ // camera can be invalid for a moment when server shuts down
+ // camera.FireNow( "Disable", "!activator", null, player )
+ camera.Destroy()
+ }
+
+ DoRespawnPlayer( player, null )
+
+ if ( PlayerCanSpawnIntoTitan( player ) )
+ {
+ table action = expect table( GenerateEmbarkActionTable( player, titan, embarkAction ) )
+ PlayerEmbarksTitan( player, titan, action )
+ }
+}
+
+entity function CreateTitanDropCamera( origin, angles )
+{
+ entity viewControl = CreateEntity( "point_viewcontrol" )
+ viewControl.kv.spawnflags = 56 // infinite hold time, snap to goal angles, make player non-solid
+
+ viewControl.SetOrigin( origin )
+ viewControl.SetAngles( angles )
+ DispatchSpawn( viewControl )
+
+ return viewControl
+}
+
+entity function CreateDropPodViewController( entity pod )
+{
+ entity viewControl = CreateEntity( "point_viewcontrol" )
+ viewControl.kv.spawnflags = 56 // infinite hold time, snap to goal angles, make player non-solid
+
+ viewControl.SetOrigin( pod.GetOrigin() + Vector( 44, -64, 520 ) )
+ float yaw = pod.GetAngles().y
+ viewControl.SetAngles( Vector( 90, yaw + 10, 0 ) )
+ DispatchSpawn( viewControl )
+
+ viewControl.SetParent( pod )
+
+ return viewControl
+}
+
+
+function ClearEntInUseOnDestroy( dropPoint, dropPod )
+{
+ dropPod.WaitSignal( "OnDestroy" )
+ dropPoint.e.spawnPointInUse = false
+}
+
+float function GetPlayerLastRespawnTime( entity player )
+{
+ return expect float( player.s.respawnTime )
+}
+
+entity function GetEmbarkPlayer( entity titan )
+{
+ if ( "embarkingPlayer" in titan.s )
+ return expect entity( titan.s.embarkingPlayer )
+
+ return null
+}
+
+entity function GetDisembarkPlayer( entity titan )
+{
+ if ( "disembarkingPlayer" in titan.s )
+ return expect entity( titan.s.disembarkingPlayer )
+
+ return null
+}
+
+entity function GetEmbarkDisembarkPlayer( entity titan )
+{
+ entity result = GetEmbarkPlayer( titan )
+
+ if ( IsValid( result ) )
+ return result
+
+ result = GetDisembarkPlayer( titan )
+ if ( IsValid( result ) )
+ return result
+
+ return null
+}
+
+void function CodeCallback_OnNPCKilled( entity npc, var damageInfo )
+{
+ if ( IsSingleplayer() )
+ {
+ OnNPCKilled_SP( npc, damageInfo )
+ return
+ }
+
+ HandleDeathPackage( npc, damageInfo )
+
+ if ( npc.IsTitan() )
+ {
+ // if a player is getting in, kill him too
+ entity player = GetEmbarkPlayer( npc )
+ if ( IsAlive( player ) )
+ {
+ // kill the embarking player
+ //printt( "Killed embarking player" )
+ KillFromInfo( player, damageInfo )
+ }
+
+ if ( !GetDoomedState( npc ) )
+ {
+ // Added via AddCallback_OnTitanDoomed
+ foreach ( callbackFunc in svGlobal.onTitanDoomedCallbacks )
+ {
+ callbackFunc( npc, damageInfo )
+ }
+ }
+ }
+
+ PlayerOrNPCKilled( npc, damageInfo )
+}
+
+void function OnNPCKilled_SP( entity npc, var damageInfo )
+{
+ HandleDeathPackage( npc, damageInfo )
+
+ if ( npc.IsTitan() )
+ {
+ // if a player is getting in, kill him too
+ entity player = GetEmbarkPlayer( npc )
+ if ( IsAlive( player ) )
+ {
+ // kill the embarking player
+ //printt( "Killed embarking player" )
+ KillFromInfo( player, damageInfo )
+ }
+
+ if ( !GetDoomedState( npc ) )
+ {
+ // Added via AddCallback_OnTitanDoomed
+ foreach ( callbackFunc in svGlobal.onTitanDoomedCallbacks )
+ {
+ callbackFunc( npc, damageInfo )
+ }
+ }
+ }
+
+ entity attacker = GetAttackerOrLastAttacker( npc, damageInfo )
+ if ( !IsValid( attacker ) )
+ return
+
+ if ( !attacker.IsPlayer() )
+ {
+ entity newAttacker = GetPlayerFromEntity( attacker )
+ if ( IsValid( newAttacker ) )
+ attacker = newAttacker
+ }
+
+ foreach( callbackFunc in svGlobal.onNPCKilledCallbacks )
+ {
+ callbackFunc( npc, attacker, damageInfo )
+ }
+
+ if ( npc.IsTitan() )
+ thread TitanVO_DelayedTitanDown( npc )
+}
+
+void function CodeCallback_OnEntityDestroyed( entity ent )
+{
+ // Must do ent.SetDoDestroyCallback( true ) to get this callback
+// print( "OnEntityDestroyed " + ent.entindex() + "\n" )
+
+ if ( "onEntityDestroyedCallbacks" in ent.s )
+ {
+ foreach ( callbackFunc in ent.s.onEntityDestroyedCallbacks )
+ {
+ callbackFunc( ent )
+ }
+ }
+}
+
+function AddEntityDestroyedCallback( ent, callbackFunc )
+{
+ AssertParameters( callbackFunc, 1, "entity" )
+
+ if ( !( "onEntityDestroyedCallbacks" in ent.s ) )
+ ent.s.onEntityDestroyedCallbacks <- []
+
+ ent.s.onEntityDestroyedCallbacks.append( callbackFunc )
+
+ // set this or else the ent won't run CodeCallback_OnEntityDestroyed at all
+ ent.SetDoDestroyCallback( true )
+}
+
+bool function WeaponInterruptsCloak( entity weapon )
+{
+ if ( !IsValid( weapon ) )
+ return false
+
+ return weapon.GetWeaponInfoFileKeyField( "does_not_interrupt_cloak" ) != 1
+}
+
+void function CodeCallback_WeaponFireInCloak( entity player )
+{
+ if ( !WeaponInterruptsCloak( player.GetActiveWeapon() ) )
+ return
+
+ if ( player.IsTitan() ) // Fix timing issue with auto-eject cloak and firing your weapon as a Titan cancelling it. This assumes we never want cloaked titans!
+ return
+
+ // if ( player.cloakedForever )
+ // {
+ // player.SetCloakFlicker( 1.0, 2.0 )
+ // return
+ // }
+
+ // // Check if we are allowed some cloaked shots based on ability selection
+ // if ( player.s.cloakedShotsAllowed > 0 )
+ // {
+ // player.s.cloakedShotsAllowed--
+ // return
+ // }
+
+ if ( IsMultiplayer() )
+ {
+ //player.SetCloakFlicker( 1.0, 2.0 )
+
+ DisableCloak( player, 0.5 )
+ entity weapon = player.GetOffhandWeapon( OFFHAND_LEFT )
+ //printt( "weapon", weapon.GetWeaponClassName() )
+ // JFS; need code feature to properly reset next attack time/cooldown stuff
+ if ( IsValid( weapon ) && weapon.GetWeaponClassName() == "mp_ability_cloak" )
+ {
+ player.TakeOffhandWeapon( OFFHAND_LEFT )
+ player.GiveOffhandWeapon( "mp_ability_cloak", OFFHAND_LEFT )
+ weapon = player.GetOffhandWeapon( OFFHAND_LEFT )
+ weapon.SetWeaponPrimaryClipCountAbsolute( 0 )
+ }
+ }
+ else
+ {
+ DisableCloak( player, 0.5 )
+ }
+}
+
+// need "you will change class next time" message
+function OnPlayerCloseClassMenu( entity player )
+{
+ if ( GetGameState() <= eGameState.Prematch )
+ return
+
+ if ( player.IsEntAlive() )
+ return
+
+ if ( player.s.inPostDeath )
+ return
+
+ if ( IsValid( player.isSpawning ) )
+ return
+
+ thread DecideRespawnPlayer( player ) // there is a wait that happens later when using rematch burncard in Frontier Defense.
+}
+
+// playerconnected Reload
+void function CodeCallback_OnClientReloadConnectionCompleted( entity player )
+{
+ FinishClientScriptInitialization( player )
+}
+
+
+bool function ShouldPlayerHaveLossProtection( entity player )
+{
+ if ( level.nv.matchProgress < GetCurrentPlaylistVarInt( "matchLossProtectionThreshold", 10 ) )
+ return false
+
+ if ( IsPrivateMatch() )
+ return false
+
+ if ( IsFFAGame() )
+ return true
+
+ int team = player.GetTeam()
+ int otherTeam = GetOtherTeam( team )
+ int teamScore = IsRoundBased() ? GameRules_GetTeamScore2( team ) : GameRules_GetTeamScore( team )
+ int otherTeamScore = IsRoundBased() ? GameRules_GetTeamScore2( otherTeam ) : GameRules_GetTeamScore( otherTeam )
+
+ if ( teamScore < otherTeamScore )
+ return true
+
+ return false
+}
+
+// This server will recieve this command from the client once they have loaded/run all of their scripts
+// Any client hud initialization should be done here
+function FinishClientScriptInitialization( entity player )
+{
+ printt( "Player client script initialization complete: " + player );
+
+ player.p.clientScriptInitialized = true
+
+ SyncServerVars( player )
+ SyncEntityVars( player )
+ SyncUIVars( player )
+
+ Remote_CallFunction_Replay( player, "ServerCallback_ClientInitComplete" )
+}
+
+function NotifyClientsOfConnection( entity player, state )
+{
+ int playerEHandle = player.GetEncodedEHandle()
+ array<entity> players = GetPlayerArray()
+ foreach ( ent in players )
+ {
+ if ( ent != player )
+ Remote_CallFunction_Replay( ent, "ServerCallback_PlayerConnectedOrDisconnected", playerEHandle, state )
+ }
+}
+
+function NotifyClientsOfTeamChange( entity player, int oldTeam, int newTeam )
+{
+ int playerEHandle = player.GetEncodedEHandle()
+ array<entity> players = GetPlayerArray()
+ foreach ( ent in players )
+ {
+ //if ( ent != player )
+ Remote_CallFunction_Replay( ent, "ServerCallback_PlayerChangedTeams", playerEHandle, oldTeam, newTeam )
+ }
+}
+
+
+bool function IsValidNPCTarget( entity ent )
+{
+ switch ( ent.GetClassName() )
+ {
+ case "npc_marvin":
+ case "npc_soldier":
+ case "npc_spectre":
+ case "npc_stalker":
+ case "npc_super_spectre":
+ case "npc_prowler":
+ case "npc_drone":
+ case "npc_titan":
+ case "npc_turret_sentry":
+ case "npc_turret_mega":
+ case "npc_dropship":
+ return true
+ }
+
+ return false
+}
+
+int function CodeCallback_GetWeaponDamageSourceId( entity weapon )
+{
+ string classname = weapon.GetWeaponClassName()
+
+ #if DEV
+ if ( ("devWeapons" in level) && classname in level.devWeapons )
+ return 0
+
+ #endif
+ //Filter out abilities for now
+ if ( !(classname in eDamageSourceId) )
+ return damagedef_unknown
+
+ //Assert( classname in getconsttable().eDamageSourceId, classname + " not added to eDamageSourceId enum" )
+ int damageSourceInt = eDamageSourceId[ classname ]
+ return damageSourceInt
+}
+
+
+
+
+function TriggerHurtSetup()
+{
+ file.hurtTriggers.extend( GetEntArrayByClass_Expensive( "trigger_hurt" ) )
+ foreach( trigger in file.hurtTriggers )
+ {
+ trigger.ConnectOutput( "OnStartTouch", TriggerHurtEnter )
+ }
+}
+
+void function TriggerHurtEnter( entity trigger, entity ent, entity caller, var value )
+{
+ if ( ent.e.destroyTriggerHurt )
+ ent.Destroy()
+}
+
+#if MP
+table< entity, table< entity, bool > > oob_triggerEntPairs
+
+void function SetupOutOfBoundsTrigger( entity trigger )
+{
+ if ( !(trigger in oob_triggerEntPairs) )
+ oob_triggerEntPairs[trigger] <- {}
+}
+#endif
+
+function OutOfBoundsSetup()
+{
+ file.outOfBoundsTriggers.extend( GetEntArrayByClass_Expensive( "trigger_out_of_bounds" ) )
+ foreach( trigger in file.outOfBoundsTriggers )
+ {
+ #if MP
+ SetupOutOfBoundsTrigger( trigger )
+ trigger.ConnectOutput( "OnStartTouch", EntityEnterOutOfBoundsTrig )
+ trigger.ConnectOutput( "OnEndTouch", EntityLeaveOutOfBoundsTrig )
+ #else
+ trigger.ConnectOutput( "OnStartTouch", EntityOutOfBounds )
+ trigger.ConnectOutput( "OnEndTouch", EntityBackInBounds )
+ #endif
+ }
+
+ AddCallback_GameStateEnter( eGameState.Postmatch, OutOfBoundsDisable )
+}
+
+void function OutOfBoundsDisable()
+{
+ foreach( trigger in file.outOfBoundsTriggers )
+ {
+ #if MP
+ foreach ( ent, val in oob_triggerEntPairs[trigger] )
+ oob_triggerEntPairs[trigger][ent] = false
+ trigger.DisconnectOutput( "OnStartTouch", EntityEnterOutOfBoundsTrig )
+ trigger.DisconnectOutput( "OnEndTouch", EntityLeaveOutOfBoundsTrig )
+ #else
+ trigger.DisconnectOutput( "OnStartTouch", EntityOutOfBounds )
+ trigger.DisconnectOutput( "OnEndTouch", EntityBackInBounds )
+ #endif
+ }
+}
+
+bool function IsPointOutOfBounds( vector point )
+{
+ foreach ( trigger in file.outOfBoundsTriggers )
+ {
+ if ( trigger.ContainsPoint( point ) )
+ return true
+ }
+ return false
+}
+
+#if MP
+void function EntityEnterOutOfBoundsTrig( entity trigger, entity ent, entity caller, var value )
+{
+ if ( !IsValid( ent ) || !ent.IsPlayer() )
+ {
+ EntityOutOfBounds( trigger, ent, null, null )
+ return
+ }
+
+ if ( !(ent in oob_triggerEntPairs[trigger]) )
+ {
+ oob_triggerEntPairs[trigger][ent] <- true
+ thread EntityCheckOutOfBoundsThread( trigger, ent )
+ }
+ else
+ {
+ oob_triggerEntPairs[trigger][ent] = true
+ // thread is already running
+ }
+}
+
+void function EntityLeaveOutOfBoundsTrig( entity trigger, entity ent, entity caller, var value )
+{
+ if ( !(ent in oob_triggerEntPairs[trigger]) )
+ {
+ EntityBackInBounds( trigger, ent, null, null )
+ return
+ }
+
+ oob_triggerEntPairs[trigger][ent] = false // tell thread to stop
+}
+
+bool function TriggerIsTouchingPlayerHullAtPoint( entity player, entity trigger, float triggerminz, vector pos, float radius )
+{
+ if ( trigger.GetClassName() == "trigger_cylinder" )
+ {
+ array<entity> touchingEnts = trigger.GetTouchingEntities()
+ return touchingEnts.contains( player )
+ }
+ else
+ {
+ return BrushTriggerIsTouchingPlayerHullAtPoint( trigger, triggerminz, pos, radius )
+ }
+
+ unreachable
+}
+
+bool function BrushTriggerIsTouchingPlayerHullAtPoint( entity trigger, float triggerminz, vector pos, float radius )
+{
+ if ( pos.z < triggerminz )
+ return false
+
+ radius *= 1.0824 // expand by 1/cos(22.5) so that an octagon circumscribes the circle
+
+ if ( trigger.ContainsPoint( pos ) ||
+ trigger.ContainsPoint( pos + <radius,0,0> ) ||
+ trigger.ContainsPoint( pos + < -radius,0,0> ) ||
+ trigger.ContainsPoint( pos + <0,radius,0> ) ||
+ trigger.ContainsPoint( pos + <0,-radius,0> ) )
+ return true
+
+ float radius45 = radius * 0.7071
+
+ if ( trigger.ContainsPoint( pos + <radius45,radius45,0> ) ||
+ trigger.ContainsPoint( pos + < -radius45,-radius45,0> ) ||
+ trigger.ContainsPoint( pos + <radius45,-radius45,0> ) ||
+ trigger.ContainsPoint( pos + < -radius45,radius45,0> ) )
+ return true
+
+ return false
+}
+
+void function EntityCheckOutOfBoundsThread( entity trigger, entity ent )
+{
+ float minz = trigger.GetOrigin().z + trigger.GetBoundingMins().z
+ float radius = ent.GetBoundingMaxs().x
+
+ bool wasTouching = false
+ for ( ;; )
+ {
+ wait 0.099
+
+ if ( !IsValid( ent ) )
+ break
+
+ if ( !oob_triggerEntPairs[trigger][ent] )
+ break
+
+ bool isTouching
+ if ( ent.IsOnGround() )
+ {
+ if ( ent.IsWallRunning() && !ent.IsWallHanging() )
+ {
+ isTouching = TriggerIsTouchingPlayerHullAtPoint( ent, trigger, minz, ent.GetOrigin() + <0,0,10>, radius )
+ }
+ else
+ {
+ isTouching = true
+ }
+ }
+ else
+ {
+ vector startpos = ent.GetOrigin()
+ vector endpos = startpos
+ endpos.z -= 2048
+
+ TraceResults result = TraceHull( startpos, endpos, ent.GetBoundingMins(), ent.GetBoundingMaxs(), ent, TRACE_MASK_PLAYERSOLID, TRACE_COLLISION_GROUP_PLAYER )
+ if ( result.startSolid || result.fraction >= 1 || TriggerIsTouchingPlayerHullAtPoint( ent, trigger, minz, result.endPos + <0,0,40>, radius ) )
+ {
+ //DebugDrawLine( startpos, result.endPos, 255,255,255, true, 3.0 )
+ isTouching = true
+ }
+ else
+ {
+ //DebugDrawLine( startpos, result.endPos, 255,0,0, true, 3.0 )
+ isTouching = false
+ }
+ }
+
+ if ( isTouching == wasTouching )
+ continue
+
+ wasTouching = isTouching
+ if ( isTouching )
+ {
+ EntityOutOfBounds( trigger, ent, null, null )
+ }
+ else
+ {
+ EntityBackInBounds( trigger, ent, null, null )
+ }
+ }
+
+ if ( wasTouching )
+ {
+ EntityBackInBounds( trigger, ent, null, null )
+ }
+
+ delete oob_triggerEntPairs[trigger][ent]
+}
+#endif
+
+void function EntityOutOfBounds( entity trigger, entity ent, entity caller, var value )
+{
+ //printt( "ENTITY", ent, "IS OUT OF BOUNDS ON TRIGGER", trigger )
+
+ if ( ent.e.destroyOutOfBounds )
+ ent.Destroy()
+
+ if ( !IsValidOutOfBoundsEntity( ent, trigger ) )
+ return
+
+ //printt( "Valid Out OfBounds Entity, EntityOutOfBounds" )
+
+ if ( !(ent in file.outOfBoundsTable) ) //Note that we never remove the ent from the table after adding it
+ {
+ OutOfBoundsDataStruct initialDataStruct
+ initialDataStruct.timeBackInBound = max( 0, Time() - OUT_OF_BOUNDS_DECAY_TIME )
+
+ ManageAddEntToOutOfBoundsTable( ent, initialDataStruct )
+ }
+
+ OutOfBoundsDataStruct dataStruct = file.outOfBoundsTable[ ent ]
+
+ dataStruct.outOfBoundsTriggersTouched++
+
+ Assert( dataStruct.outOfBoundsTriggersTouched > 0 )
+
+ // Not already touching another trigger
+ if ( dataStruct.outOfBoundsTriggersTouched == 1 )
+ {
+ float decayTime = max( 0, Time() - dataStruct.timeBackInBound - OUT_OF_BOUNDS_DECAY_DELAY )
+ float outOfBoundsTimeRegained = decayTime * ( OUT_OF_BOUNDS_TIME_LIMIT / OUT_OF_BOUNDS_DECAY_TIME )
+ float deadTime = clamp( dataStruct.timeLeftBeforeDyingFromOutOfBounds + outOfBoundsTimeRegained, 0.0, OUT_OF_BOUNDS_TIME_LIMIT )
+
+ //printt( "Decay Time: " + decayTime + ", outOfBoundsTimeRegained:" + outOfBoundsTimeRegained + ", timeLeftBeforeDyingFromOutOfBounds: " + dataStruct.timeLeftBeforeDyingFromOutOfBounds + ", deadTime: " + deadTime )
+
+ dataStruct.timeLeftBeforeDyingFromOutOfBounds = deadTime
+
+ ent.SetOutOfBoundsDeadTime( Time() + deadTime )
+
+ thread KillEntityOutOfBounds( ent, trigger )
+ }
+
+ //printt( "ent.GetOutOfBoundsDeadTime():", ent.GetOutOfBoundsDeadTime() )
+}
+
+bool function EntityIsOutOfBounds( entity ent )
+{
+ if ( !( ent in file.outOfBoundsTable ) )
+ return false
+ return file.outOfBoundsTable[ ent ].outOfBoundsTriggersTouched > 0
+}
+
+void function EntityBackInBounds( entity trigger, entity ent, entity caller, var value )
+{
+ //printt( "ENTITY", ent, "IS BACK IN BOUNDS OF TRIGGER", trigger )
+
+ if ( !IsValidOutOfBoundsEntity( ent, trigger ) )
+ return
+
+ //printt( "Valid Out OfBounds Entity, EntityBackInBounds" )
+
+ if ( !(ent in file.outOfBoundsTable) ) //Can go back in bounds even though we went out of bounds as an invalid ent, e.g. in a dropship
+ {
+ OutOfBoundsDataStruct initialDataStruct
+ ManageAddEntToOutOfBoundsTable( ent, initialDataStruct )
+
+ ent.SetOutOfBoundsDeadTime( 0.0 )
+ ent.Signal( "BackInBounds" )
+
+ return
+ }
+ else
+ {
+ OutOfBoundsDataStruct dataStruct = file.outOfBoundsTable[ ent ]
+
+ dataStruct.outOfBoundsTriggersTouched--
+ if ( dataStruct.outOfBoundsTriggersTouched < 0 ) //You can exit from bounds while being an invalid ent from out of bounds on the way in, e.g. during dropship anims, etc
+ dataStruct.outOfBoundsTriggersTouched = 0
+
+ if ( dataStruct.outOfBoundsTriggersTouched == 0 )
+ {
+ dataStruct.timeBackInBound = Time()
+ dataStruct.timeLeftBeforeDyingFromOutOfBounds = max( 0, ent.GetOutOfBoundsDeadTime() - Time() )
+ ent.SetOutOfBoundsDeadTime( 0.0 )
+ ent.Signal( "BackInBounds" )
+ return
+ }
+ }
+}
+
+void function KillEntityOutOfBounds( entity ent, entity trigger )
+{
+ if ( GetGameState() < eGameState.Playing )
+ return
+
+ Assert( ent.GetOutOfBoundsDeadTime() != 0 )
+ Assert( Time() <= ent.GetOutOfBoundsDeadTime() )
+
+ ent.EndSignal( "OnDeath" )
+ ent.Signal( "OutOfBounds" )
+ ent.EndSignal( "OutOfBounds" )
+ ent.EndSignal( "BackInBounds" )
+
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) && !IsAlive( ent ) )
+ {
+ file.outOfBoundsTable[ ent ].outOfBoundsTriggersTouched = 0
+ ent.SetOutOfBoundsDeadTime( 0 )
+ }
+ }
+ )
+
+ wait ent.GetOutOfBoundsDeadTime() - Time()
+
+ if ( !IsValidOutOfBoundsEntity( ent, trigger ) )
+ return
+
+ if ( ent.GetOutOfBoundsDeadTime() == 0 )
+ return
+
+ ent.Die( svGlobal.worldspawn, svGlobal.worldspawn, { scriptType = DF_INSTANT, damageSourceId = eDamageSourceId.outOfBounds } )
+}
+
+bool function IsValidOutOfBoundsEntity( entity ent, entity trigger )
+{
+ if ( !IsValid( ent ) )
+ return false
+
+ if ( !IsAlive( ent ) )
+ return false
+
+ int triggerTeam = expect int( trigger.kv.teamnumber.tointeger() )
+
+ Assert( triggerTeam >= 0 )
+
+ if ( triggerTeam != 0 && ent.GetTeam() != triggerTeam )
+ return false
+
+ // Temp hack for tday intro, might not keep this
+ if ( "disableOutOfBounds" in level && level.disableOutOfBounds == true )
+ return false
+
+ if ( ent.IsPlayer() )
+ {
+ if ( ent.IsNoclipping() && !ent.Anim_IsActive() ) //Need to check for Anim_IsActive because PlayAnim() calls will set IsNoclipping() to true. This caused a bug with ejecting out of a OutOfBounds trigger
+ return false
+
+ entity parentEnt = ent.GetParent()
+ if ( IsValid( parentEnt ) && IsDropship( parentEnt ) )
+ return false
+
+ return true
+ }
+
+ if ( ent.IsNPC() && ent.IsTitan() )
+ return true
+
+ return false
+}
+
+void function OnTitanBecomesPilot_OutOfBoundsCheck( entity pilot, entity npc_titan )
+{
+ if ( pilot.GetOutOfBoundsDeadTime() == 0 )
+ return
+
+ npc_titan.SetOrigin( npc_titan.GetOrigin() ) //Kinda a hack to force redetection of the Titan touching the out of bounds trigger
+}
+
+void function ManageAddEntToOutOfBoundsTable( entity ent, OutOfBoundsDataStruct dataStruct ) //Might be overkill, but: suggested by Haggerty to avoid leak of constantly adding ents to the file table without removing them
+{
+ //First clean up dead references in table
+ table< entity, OutOfBoundsDataStruct> tempTable = clone file.outOfBoundsTable
+
+ foreach( ent, dataStruct in tempTable )
+ {
+ if ( !IsValid( ent ) )
+ {
+ delete file.outOfBoundsTable[ ent ]
+ }
+ }
+
+ //Now add the new ent
+
+ file.outOfBoundsTable[ ent ] <- dataStruct
+}
+
+bool function PlayerCanSpawn( entity player )
+{
+ if ( IsAlive( player ) )
+ return false
+
+ if ( player.isSpawning )
+ return false
+
+ return true
+}
+
+function SetTitanAvailable( entity player )
+{
+ Assert( player.entindex() < 32 )
+ int shiftIndex = player.entindex() - 1
+ int elimMask = (1 << shiftIndex)
+
+ level.nv.titanAvailableBits = level.nv.titanAvailableBits | elimMask
+
+ #if MP
+ PIN_PlayerAbilityReady( player, "titanfall" )
+ #endif
+}
+
+function ClearTitanAvailable( entity player )
+{
+ Assert( player.entindex() < 32 )
+ int shiftIndex = player.entindex() - 1
+ int elimMask = (1 << shiftIndex)
+
+ level.nv.titanAvailableBits = level.nv.titanAvailableBits & (~elimMask)
+}
+
+
+
+function SetRespawnAvailable( entity player )
+{
+ Assert( player.entindex() < 32 )
+ int shiftIndex = player.entindex() - 1
+ int elimMask = (1 << shiftIndex)
+
+ level.nv.respawnAvailableBits = level.nv.respawnAvailableBits | elimMask
+}
+
+
+function ClearRespawnAvailable( entity player )
+{
+ Assert( player.entindex() < 32 )
+ int shiftIndex = player.entindex() - 1
+ int elimMask = (1 << shiftIndex)
+
+ level.nv.respawnAvailableBits = level.nv.respawnAvailableBits & (~elimMask)
+}
+
+
+void function SetPlayerEliminated( entity player )
+{
+ player.SetPlayerGameStat( PGS_ELIMINATED, 1 )
+}
+
+void function ClearPlayerEliminated( entity player )
+{
+ player.SetPlayerGameStat( PGS_ELIMINATED, 0 )
+}
+
+bool function IsPlayerEliminated( entity player )
+{
+ return (player.GetPlayerGameStat( PGS_ELIMINATED ) > 0)
+}
+
+bool function IsTeamEliminated( int team )
+{
+ array<entity> players = GetPlayerArrayOfTeam( team )
+
+ foreach ( player in players )
+ {
+ if ( IsPlayerEliminated( player ) != true )
+ return false
+ }
+
+ return true
+}
+
+// Clears all scoreboard data for the player to make sure we never use old data
+void function ClearPostGameScoreboardData( entity player )
+{
+ if ( !IsValid( player ) || !player.IsPlayer() )
+ return
+
+ player.SetPersistentVar( "isPostGameScoreboardValid", false )
+ player.SetPersistentVar( "isFDPostGameScoreboardValid", false )
+}
+
+bool function ShouldShowLossProtectionOnEOG( entity player )
+{
+ if ( player.p.hasMatchLossProtection != true )
+ return false
+
+ if ( player.GetTeam() == GetWinningTeam() )
+ return false
+
+ if ( IsPrivateMatch() )
+ return false
+
+ return true
+}
+
+bool function GameModeRemove( entity ent )
+{
+ string gameMode = GameRules_GetGameMode()
+ switch ( gameMode )
+ {
+ // These game modes have checkboxes in leveled
+ case LAST_TITAN_STANDING:
+ case TEAM_DEATHMATCH:
+ case ATTRITION:
+ case CAPTURE_POINT:
+ case CAPTURE_THE_FLAG:
+ case FORT_WAR:
+ case FFA:
+ case FD:
+ break
+
+ // These game modes use tdm spawns
+ case PILOT_SKIRMISH:
+ case WINGMAN_PILOT_SKIRMISH:
+ case MARKED_FOR_DEATH_PRO:
+ case MARKED_FOR_DEATH:
+ case T_DAY:
+ case AI_TDM:
+ case BOMB:
+ case HARDCORE_TDM:
+ case COLISEUM:
+ case HUNTED:
+ case DON:
+ case TITAN_BRAWL:
+ case SPEEDBALL:
+ gameMode = TEAM_DEATHMATCH
+ break
+
+ case RAID:
+ case ATCOOP:
+ case CONQUEST:
+ case PVE_SANDBOX:
+ gameMode = ATTRITION
+ break
+
+ case LTS_BOMB:
+ case WINGMAN_LAST_TITAN_STANDING:
+ gameMode = LAST_TITAN_STANDING
+ break
+
+ case FREE_AGENCY:
+ gameMode = FFA
+ break
+
+ default:
+ // If a game mode is not handled in here, spawnpoints won't have checkboxes that correspond to it, so all spawnpoints will be used in that mode, which is probably bad.
+ Assert( false, "Game mode " + gameMode + " not handled in GameModeRemove()" )
+ }
+
+ AT_CollisionCleanup( ent )
+
+ string gamemodeKey = "gamemode_" + gameMode
+ if ( ent.HasKey( gamemodeKey ) && (ent.kv[gamemodeKey] == "0" || ent.kv[gamemodeKey] == "") )
+ {
+ // printt( "Removing ent " + ent.GetClassName() + " with " + gamemodeKey + " = \"" + ent.kv[gamemodeKey] + "\" at " + ent.GetOrigin() )
+ ent.Destroy()
+ return true
+ }
+ //printt( "keeping ent", ent.GetClassName() )
+
+ return false
+}
+
+void function AT_CollisionCleanup( entity spawnPoint )
+{
+ if ( spawnPoint.GetScriptName() == "at_mega_turret" )
+ {
+ if ( spawnPoint.GetLinkEnt() != null ) // assuming this is func_brush_navmesh_separator
+ {
+ entity brush = spawnPoint.GetLinkEnt()
+ brush.NotSolid()
+ }
+ }
+}
+
+
+void function EntityFire( entity ent, string fire )
+{
+ ent.Fire( fire )
+}
+
+void function EntityFireDelayed( entity ent, string fire, string parm, float delay )
+{
+ ent.Fire( fire, parm, delay )
+}
+
+#if MP
+void function AddOutOfBoundsTriggerWithParams( vector org, float radius = 250.0, float height = 250.0 )
+{
+ entity trigger = CreateEntity( "trigger_cylinder" )
+ trigger.SetRadius( radius )
+ trigger.SetAboveHeight( height ) //Still not quite a sphere, will see if close enough
+ trigger.SetBelowHeight( height )
+ trigger.SetOrigin( org )
+ DispatchSpawn( trigger )
+ SetupOutOfBoundsTrigger( trigger )
+ trigger.SetEnterCallback( OnOOBTriggerEnter )
+ trigger.SetLeaveCallback( OnOOBTriggerLeave )
+}
+
+void function OnOOBTriggerEnter( entity trigger, entity ent )
+{
+ EntityEnterOutOfBoundsTrig( trigger, ent, null, 0 )
+}
+
+void function OnOOBTriggerLeave( entity trigger, entity ent )
+{
+ EntityLeaveOutOfBoundsTrig( trigger, ent, null, 0 )
+}
+#endif \ No newline at end of file