From 9a96d0bff56f1969c68bb52a2f33296095bdc67d Mon Sep 17 00:00:00 2001 From: BobTheBob <32057864+BobTheBob9@users.noreply.github.com> Date: Tue, 31 Aug 2021 23:14:58 +0100 Subject: move to new mod format --- .../mod/scripts/vscripts/mp/_base_gametype.gnut | 2179 ++++++++++++++++++++ 1 file changed, 2179 insertions(+) create mode 100644 Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut (limited to 'Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype.gnut') 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 outOfBoundsTriggers = [] + array 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 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 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 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, arrayargs ) +{ + 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 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 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 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 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 + ) || + 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 + ) || + trigger.ContainsPoint( pos + < -radius45,-radius45,0> ) || + trigger.ContainsPoint( pos + ) || + 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 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 -- cgit v1.2.3