aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/scripts/vscripts/mp
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.CustomServers/scripts/vscripts/mp')
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_ai_mp.gnut41
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_ai_mp.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_ai_superspectre.nut736
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_base_gametype.gnut2179
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_base_gametype_mp.gnut586
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_battery_port.gnut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_bleedout.gnut403
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_challenges.gnut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_changemap.nut24
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_classic_mp.nut67
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut230
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_classic_mp_no_intro.gnut46
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_codecallbacks.gnut999
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_dropship_spawn_common.gnut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_gamestate.nut144
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_gamestate_mp.nut707
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_goblin_dropship.nut784
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_lasermesh.gnut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_loadout_crate.nut183
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_mp_mapspawn.gnut65
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_music.gnut107
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_pickups.gnut1195
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_pickups_glow.gnut53
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_playlist.gnut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_revive.gnut352
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_score.nut133
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_serverflags.nut35
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_sniper_spectres.nut485
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_spawn_functions.nut60
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_spectre_rack.nut395
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_stats.nut78
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_titan_npc.nut818
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_titan_tether.gnut307
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_titan_transfer.nut641
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_tonecontroller.nut189
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_utility_mp.gnut18
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/_vr.nut66
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/_lf_maps_shared.gnut8
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_angel_city.nut11
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_angel_city_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_black_water_canal.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_black_water_canal_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_coliseum.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_coliseum_column.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_colony02.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_colony02_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_complex3.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_crashsite3.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_drydock.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_drydock_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_eden.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_forwardbase_kodai.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_forwardbase_kodai_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_glitch.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_glitch_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_grave.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_grave_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_homestead.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_homestead_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_deck.nut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_meadow.nut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_stacks.nut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_township.nut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_traffic.nut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_uma.nut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_relic02.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_relic02_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_rise.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_rise_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_thaw.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_thaw_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_wargames.nut415
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/levels/mp_wargames_fd.nut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/pintelemetry.gnut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/player_cloak.nut184
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/spawn.nut428
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/spawn_debug.gnut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/spawn_on_friendly.gnut1
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/spawn_wave.gnut6
-rw-r--r--Northstar.CustomServers/scripts/vscripts/mp/spawn_wave_dropship.gnut1
80 files changed, 13266 insertions, 0 deletions
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_ai_mp.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_ai_mp.gnut
new file mode 100644
index 000000000..ac0c309b7
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_ai_mp.gnut
@@ -0,0 +1,41 @@
+global function MpInitAILoadouts
+global function SetProficiency
+global function IsAutoPopulateEnabled
+global function SPMP_UpdateNPCProficiency
+global function SPMP_Callback_ForceAIMissPlayer
+
+void function MpInitAILoadouts()
+{
+
+}
+
+void function SetProficiency( entity soldier )
+{
+
+}
+
+bool function IsAutoPopulateEnabled( var team = null )
+{
+ if ( IsNPCSpawningEnabled() == false )
+ return false
+
+ if ( Flag( "disable_npcs" ) )
+ return false
+
+ if ( team == TEAM_MILITIA && Flag( "Disable_MILITIA" ) )
+ return false
+ if ( team == TEAM_IMC && Flag( "Disable_IMC" ) )
+ return false
+
+ return true
+}
+
+void function SPMP_UpdateNPCProficiency(entity ent)
+{
+
+}
+
+bool function SPMP_Callback_ForceAIMissPlayer(entity npc, entity player)
+{
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_ai_mp.nut b/Northstar.CustomServers/scripts/vscripts/mp/_ai_mp.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_ai_mp.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_ai_superspectre.nut b/Northstar.CustomServers/scripts/vscripts/mp/_ai_superspectre.nut
new file mode 100644
index 000000000..68e888f41
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_ai_superspectre.nut
@@ -0,0 +1,736 @@
+untyped
+
+global function AiSuperspectre_Init
+
+global function SuperSpectre_OnGroundSlamImpact
+global function SuperSpectre_OnGroundLandImpact
+global function SuperSpectreThink
+global function SuperSpectreOnLeeched
+global function SuperSpectre_WarpFall
+global function CreateExplosionInflictor
+global function FragDroneDeplyAnimation
+global function ForceTickLaunch
+
+global function Reaper_LaunchFragDrone_Think
+global function ReaperMinionLauncherThink
+
+//==============================================================
+// AI Super Spectre
+//
+// Super Spectre keeps an array of the minions it spawned.
+// Each of those minions has a reference back to it's "master."
+//==============================================================
+const FRAG_DRONE_BATCH_COUNT = 10
+const FRAG_DRONE_IN_FRONT_COUNT = 2
+const FRAG_DRONE_MIN_LAUNCH_COUNT = 4
+const FRAG_DRONE_LAUNCH_INTIAL_DELAY_MIN = 10
+const FRAG_DRONE_LAUNCH_INTIAL_DELAY_MAX = 20
+const FRAG_DRONE_LAUNCH_INTERVAL = 40
+const SPAWN_ENEMY_TOO_CLOSE_RANGE_SQR = 1048576 // Don't spawn guys if the target enemy is closer than this range (1024^2).
+const SPAWN_HIDDEN_ENEMY_WITHIN_RANGE_SQR = 1048576 // If the enemy can't bee seen, and they are within in this range (1024^2), spawn dudes to find him.
+const SPAWN_ENEMY_ABOVE_HEIGHT = 128 // If the enemy is at least this high up, then spawn dudes to find him.
+const SPAWN_FUSE_TIME = 2.0 // How long after being fired before the spawner explodes and spawns a spectre.
+const SPAWN_PROJECTILE_AIR_TIME = 3.0 // How long the spawn project will be in the air before hitting the ground.
+const SPECTRE_EXPLOSION_DMG_MULTIPLIER = 1.2 // +20%
+const DEV_DEBUG_PRINTS = false
+
+struct
+{
+ int activeMinions_GlobalArrayIdx = -1
+} file
+
+function AiSuperspectre_Init()
+{
+ PrecacheParticleSystem( $"P_sup_spectre_death" )
+ PrecacheParticleSystem( $"P_sup_spectre_death_nuke" )
+ PrecacheParticleSystem( $"P_xo_damage_fire_2" )
+ PrecacheParticleSystem( $"P_sup_spec_dam_vent_1" )
+ PrecacheParticleSystem( $"P_sup_spec_dam_vent_2" )
+ PrecacheParticleSystem( $"P_sup_spectre_dam_1" )
+ PrecacheParticleSystem( $"P_sup_spectre_dam_2" )
+ PrecacheParticleSystem( $"drone_dam_smoke_2" )
+ PrecacheParticleSystem( $"P_wpn_muzzleflash_sspectre" )
+
+ PrecacheImpactEffectTable( "superSpectre_groundSlam_impact" )
+ PrecacheImpactEffectTable( "superSpectre_megajump_land" )
+
+ RegisterSignal( "SuperSpectre_OnGroundSlamImpact" )
+ RegisterSignal( "SuperSpectre_OnGroundLandImpact" )
+ RegisterSignal( "SuperSpectreThinkRunning" )
+ RegisterSignal( "OnNukeBreakingDamage" ) // enough damage to break out or skip nuke
+ RegisterSignal( "death_explosion" )
+ RegisterSignal( "WarpfallComplete" )
+ RegisterSignal( "BeginLaunchAttack" )
+
+ AddDeathCallback( "npc_super_spectre", SuperSpectreDeath )
+ AddDamageCallback( "npc_super_spectre", SuperSpectre_OnDamage )
+ //AddPostDamageCallback( "npc_super_spectre", SuperSpectre_PostDamage )
+
+ file.activeMinions_GlobalArrayIdx = CreateScriptManagedEntArray()
+}
+
+void function SuperSpectre_OnDamage( entity npc, var damageInfo )
+{
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( damageSourceId == eDamageSourceId.suicideSpectreAoE )
+ {
+ // super spectre takes reduced damage from suicide spectres
+ DamageInfo_ScaleDamage( damageInfo, 0.666 )
+ }
+}
+
+void function SuperSpectre_PostDamage( entity npc, var damageInfo )
+{
+ float switchRatio = 0.33
+ float ratio = HealthRatio( npc )
+ if ( ratio < switchRatio )
+ return
+ float newRatio = ( npc.GetHealth() - DamageInfo_GetDamage( damageInfo ) ) / npc.GetMaxHealth()
+ if ( newRatio >= switchRatio )
+ return
+
+ // destroy body groups
+ int bodygroup
+ bodygroup = npc.FindBodyGroup( "lowerbody" )
+ npc.SetBodygroup( bodygroup, 1 )
+ bodygroup = npc.FindBodyGroup( "upperbody" )
+ npc.SetBodygroup( bodygroup, 1 )
+}
+
+void function SuperSpectreDeath( entity npc, var damageInfo )
+{
+ thread DoSuperSpectreDeath( npc, damageInfo )
+}
+
+void function SuperSpectreNukes( entity npc, entity attacker )
+{
+ npc.EndSignal( "OnDestroy" )
+ vector origin = npc.GetWorldSpaceCenter()
+ EmitSoundAtPosition( npc.GetTeam(), origin, "ai_reaper_nukedestruct_explo_3p" )
+ PlayFX( $"P_sup_spectre_death_nuke", origin, npc.GetAngles() )
+
+ thread SuperSpectreNukeDamage( npc.GetTeam(), origin, attacker )
+ WaitFrame() // so effect has time to grow and cover the swap to gibs
+ npc.Gib( <0,0,100> )
+}
+
+void function DoSuperSpectreDeath( entity npc, var damageInfo )
+{
+ // destroyed?
+ if ( !IsValid( npc ) )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ const int SUPER_SPECTRE_NUKE_DEATH_THRESHOLD = 300
+
+ bool giveBattery = ( npc.ai.shouldDropBattery && IsSingleplayer() )
+
+ if ( !ShouldNukeOnDeath( npc ) || !npc.IsOnGround() || !npc.IsInterruptable() || DamageInfo_GetDamage( damageInfo ) > SUPER_SPECTRE_NUKE_DEATH_THRESHOLD || ( IsValid( attacker ) && attacker.IsTitan() ) )
+ {
+ // just boom
+ vector origin = npc.GetWorldSpaceCenter()
+ EmitSoundAtPosition( npc.GetTeam(), origin, "ai_reaper_explo_3p" )
+ npc.Gib( DamageInfo_GetDamageForce( damageInfo ) )
+ if ( giveBattery )
+ SpawnTitanBatteryOnDeath( npc, null )
+
+ return
+ }
+
+ npc.ai.killShotSound = false
+ npc.EndSignal( "OnDestroy" )
+
+ entity nukeFXInfoTarget = CreateEntity( "info_target" )
+ nukeFXInfoTarget.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ DispatchSpawn( nukeFXInfoTarget )
+
+ nukeFXInfoTarget.SetParent( npc, "HIJACK" )
+
+ EmitSoundOnEntity( nukeFXInfoTarget, "ai_reaper_nukedestruct_warmup_3p" )
+
+ AI_CreateDangerousArea_DamageDef( damagedef_reaper_nuke, nukeFXInfoTarget, TEAM_INVALID, true, true )
+
+ OnThreadEnd(
+ function() : ( nukeFXInfoTarget, npc, attacker, giveBattery )
+ {
+ if ( IsValid( nukeFXInfoTarget ) )
+ {
+ StopSoundOnEntity( nukeFXInfoTarget, "ai_reaper_nukedestruct_warmup_3p" )
+ nukeFXInfoTarget.Destroy()
+ }
+
+
+ if ( IsValid( npc ) )
+ {
+ thread SuperSpectreNukes( npc, attacker )
+ if ( giveBattery )
+ {
+ SpawnTitanBatteryOnDeath( npc, null )
+ }
+ }
+ }
+ )
+
+ //int bodygroup = npc.FindBodyGroup( "upperbody" )
+ //npc.SetBodygroup( bodygroup, 1 )
+
+ // TODO: Add death sound
+
+ WaitSignalOnDeadEnt( npc, "death_explosion" )
+}
+
+entity function CreateExplosionInflictor( vector origin )
+{
+ entity inflictor = CreateEntity( "script_ref" )
+ inflictor.SetOrigin( origin )
+ inflictor.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ DispatchSpawn( inflictor )
+ return inflictor
+}
+
+void function SuperSpectreNukeDamage( int team, vector origin, entity attacker )
+{
+ // all damage must have an inflictor currently
+ entity inflictor = CreateExplosionInflictor( origin )
+
+ OnThreadEnd(
+ function() : ( inflictor )
+ {
+ if ( IsValid( inflictor ) )
+ inflictor.Destroy()
+ }
+ )
+
+ int explosions = 8
+ float time = 1.0
+
+ for ( int i = 0; i < explosions; i++ )
+ {
+ entity explosionOwner
+ if ( IsValid( attacker ) )
+ explosionOwner = attacker
+ else
+ explosionOwner = GetTeamEnt( team )
+
+ RadiusDamage_DamageDefSimple(
+ damagedef_reaper_nuke,
+ origin, // origin
+ explosionOwner, // owner
+ inflictor, // inflictor
+ 0 ) // dist from attacker
+
+ wait RandomFloatRange( 0.01, 0.21 )
+ }
+}
+
+void function SuperSpectre_OnGroundLandImpact( entity npc )
+{
+ PlayImpactFXTable( npc.GetOrigin(), npc, "superSpectre_megajump_land", SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+}
+
+
+void function SuperSpectre_OnGroundSlamImpact( entity npc )
+{
+ PlayGroundSlamFX( npc )
+}
+
+
+function PlayGroundSlamFX( entity npc )
+{
+ int attachment = npc.LookupAttachment( "muzzle_flash" )
+ vector origin = npc.GetAttachmentOrigin( attachment )
+ PlayImpactFXTable( origin, npc, "superSpectre_groundSlam_impact", SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+}
+
+
+bool function EnemyWithinRangeSqr( entity npc, entity enemy, float range )
+{
+ vector pos = npc.GetOrigin()
+ vector enemyPos = enemy.GetOrigin()
+ float distance = DistanceSqr( pos, enemyPos )
+
+ return distance <= range
+}
+
+bool function ShouldLaunchFragDrones( entity npc, int activeMinions_EntArrayID )
+{
+// printt( "active " + GetScriptManagedEntArrayLen( activeMinions_EntArrayID ) )
+ if ( !npc.ai.superSpectreEnableFragDrones )
+ return false
+
+ // check global minions
+ if ( GetScriptManagedEntArrayLen( file.activeMinions_GlobalArrayIdx ) > 5 )
+ return false
+
+ // only launch if all minions are dead
+ if ( GetScriptManagedEntArrayLen( activeMinions_EntArrayID ) > 5 )
+ return false
+
+ entity enemy = npc.GetEnemy()
+
+ // Only spawn dudes if we have an enemy
+ if ( !IsValid( enemy ) )
+ return false
+
+ vector ornull lkp = npc.LastKnownPosition( enemy )
+ if ( lkp == null )
+ return false
+
+ expect vector( lkp )
+
+ // Don't spawn if the enemy is too far away
+ if ( Distance( npc.GetOrigin(), lkp ) > 1500 )
+ return false
+
+ return true
+}
+
+function SuperSpectreOnLeeched( npc, player )
+{
+ local maxHealth = npc.GetMaxHealth()
+ npc.SetHealth( maxHealth * 0.5 ) // refill to half health
+}
+
+function SuperSpectreThink( entity npc )
+{
+ npc.EndSignal( "OnDeath" )
+
+ int team = npc.GetTeam()
+
+ int activeMinions_EntArrayID = CreateScriptManagedEntArray()
+ if ( npc.kv.squadname == "" )
+ SetSquad( npc, UniqueString( "super_spec_squad" ) )
+
+ npc.ai.superSpectreEnableFragDrones = expect int( npc.Dev_GetAISettingByKeyField( "enable_frag_drones" ) ) == 1
+
+ OnThreadEnd (
+ function() : ( activeMinions_EntArrayID, npc, team )
+ {
+ entity owner
+ if ( IsValid( npc ) )
+ owner = npc
+
+ foreach ( minion in GetScriptManagedEntArray( activeMinions_EntArrayID ) )
+ {
+ // Self destruct the suicide spectres if applicable
+ if ( minion.GetClassName() != "npc_frag_drone" )
+ continue
+
+ if ( minion.ai.suicideSpectreExplodingAttacker == null )
+ minion.TakeDamage( minion.GetHealth(), owner, owner, { scriptType = DF_DOOMED_HEALTH_LOSS, damageSourceId = eDamageSourceId.mp_weapon_super_spectre } )
+ }
+ }
+ )
+
+ wait RandomFloatRange( FRAG_DRONE_LAUNCH_INTIAL_DELAY_MIN, FRAG_DRONE_LAUNCH_INTIAL_DELAY_MAX )
+
+ npc.kv.doScheduleChangeSignal = true
+
+ while ( 1 )
+ {
+ if ( ShouldLaunchFragDrones( npc, activeMinions_EntArrayID ) )
+ waitthread SuperSpectre_LaunchFragDrone_Think( npc, activeMinions_EntArrayID )
+
+ wait FRAG_DRONE_LAUNCH_INTERVAL
+ }
+}
+
+void function SuperSpectre_LaunchFragDrone_Think( entity npc, int activeMinions_EntArrayID )
+{
+ array<vector> targetOrigins = GetFragDroneTargetOrigins( npc, npc.GetOrigin(), 200, 2000, 64, FRAG_DRONE_BATCH_COUNT )
+
+ if ( targetOrigins.len() < FRAG_DRONE_MIN_LAUNCH_COUNT )
+ return
+
+ npc.RequestSpecialRangeAttack( targetOrigins.len() + FRAG_DRONE_IN_FRONT_COUNT )
+
+ // wait for first attack signal
+ npc.WaitSignal( "OnSpecialAttack" )
+ npc.EndSignal( "OnDeath" )
+ npc.EndSignal( "OnScheduleChange" ) // kv.doScheduleChangeSignal = true
+
+ // drop a few in front of enemy view
+ entity enemy = npc.GetEnemy()
+ if ( enemy )
+ {
+ vector searchOrigin = enemy.GetOrigin() + ( enemy.GetForwardVector() * 400 )
+ array<vector> frontOfEnemyOrigins = GetFragDroneTargetOrigins( npc, searchOrigin, 0, 500, 16, FRAG_DRONE_IN_FRONT_COUNT )
+
+ foreach ( targetOrigin in frontOfEnemyOrigins )
+ {
+ thread LaunchSpawnerProjectile( npc, targetOrigin, activeMinions_EntArrayID )
+ //DebugDrawBox( targetOrigin, Vector(-10, -10, 0), Vector(10, 10, 10), 255, 0, 0, 255, 5 )
+ npc.WaitSignal( "OnSpecialAttack" )
+ }
+ }
+
+ // drop rest in pre-searched spots
+ foreach ( targetOrigin in targetOrigins )
+ {
+ thread LaunchSpawnerProjectile( npc, targetOrigin, activeMinions_EntArrayID )
+ npc.WaitSignal( "OnSpecialAttack" )
+ }
+}
+
+void function ReaperMinionLauncherThink( entity reaper )
+{
+ if ( GetBugReproNum() != 221936 )
+ reaper.kv.squadname = ""
+
+ StationaryAIPosition launchPos = GetClosestAvailableStationaryPosition( reaper.GetOrigin(), 8000, eStationaryAIPositionTypes.LAUNCHER_REAPER )
+ launchPos.inUse = true
+
+ OnThreadEnd(
+ function () : ( launchPos )
+ {
+ launchPos.inUse = false
+ }
+ )
+
+ reaper.EndSignal( "OnDeath" )
+ reaper.AssaultSetFightRadius( 96 )
+ reaper.AssaultSetGoalRadius( reaper.GetMinGoalRadius() )
+
+ while ( true )
+ {
+ WaitFrame()
+
+ if ( Distance( reaper.GetOrigin(), launchPos.origin ) > 96 )
+ {
+ printt( reaper," ASSAULT:", launchPos.origin, Distance( reaper.GetOrigin(), launchPos.origin ) )
+ reaper.AssaultPoint( launchPos.origin )
+ table signalData = WaitSignal( reaper, "OnFinishedAssault", "OnEnterGoalRadius", "OnFailedToPath" )
+ printt( reaper," END ASSAULT:", launchPos.origin, signalData.signal )
+ if ( signalData.signal == "OnFailedToPath" )
+ continue
+ }
+
+ printt( reaper," LAUNCH:", launchPos.origin )
+ waitthread Reaper_LaunchFragDrone_Think( reaper, "npc_frag_drone_fd" )
+ printt( reaper," END LAUNCH:", launchPos.origin )
+ while ( GetScriptManagedEntArrayLen( reaper.ai.activeMinionEntArrayID ) > 2 )
+ WaitFrame()
+ }
+}
+
+void function Reaper_LaunchFragDrone_Think( entity reaper, string fragDroneSettings = "" )
+{
+ if ( reaper.ai.activeMinionEntArrayID < 0 )
+ reaper.ai.activeMinionEntArrayID = CreateScriptManagedEntArray()
+
+ int activeMinions_EntArrayID = reaper.ai.activeMinionEntArrayID
+
+ const int MAX_TICKS = 4
+
+ int currentMinions = GetScriptManagedEntArray( reaper.ai.activeMinionEntArrayID ).len()
+ int minionsToSpawn = MAX_TICKS - currentMinions
+
+ if ( minionsToSpawn <= 0 )
+ return
+
+ array<vector> targetOrigins = GetFragDroneTargetOrigins( reaper, reaper.GetOrigin(), 200, 2000, 64, MAX_TICKS )
+
+ if ( targetOrigins.len() < minionsToSpawn )
+ return
+
+ if ( IsAlive( reaper.GetEnemy() ) && ( reaper.GetEnemy().IsPlayer() || reaper.GetEnemy().IsNPC() ) && reaper.CanSee( reaper.GetEnemy() ) )
+ return
+
+ OnThreadEnd(
+ function() : ( reaper )
+ {
+ if ( IsValid( reaper ) )
+ {
+ reaper.Anim_Stop()
+ }
+ }
+ )
+
+ printt( reaper, " BEGIN LAUNCHING: ", minionsToSpawn, reaper.GetCurScheduleName() )
+
+ reaper.EndSignal( "OnDeath" )
+
+ while ( !reaper.IsInterruptable() )
+ WaitFrame()
+
+ waitthread PlayAnim( reaper, "sspec_idle_to_speclaunch" )
+
+ while ( minionsToSpawn > 0 )
+ {
+ // drop rest in pre-searched spots
+ foreach ( targetOrigin in targetOrigins )
+ {
+ if ( minionsToSpawn <= 0 )
+ break
+
+ printt( reaper, " LAUNCHING: ", minionsToSpawn )
+ thread LaunchSpawnerProjectile( reaper, targetOrigin, activeMinions_EntArrayID, fragDroneSettings )
+ minionsToSpawn--
+
+ if ( minionsToSpawn <= 0 )
+ break
+
+ waitthread PlayAnim( reaper, "sspec_speclaunch_fire" )
+ }
+ }
+
+ waitthread PlayAnim( reaper, "sspec_speclaunch_to_idle" )
+}
+
+
+
+array<vector> function GetFragDroneTargetOrigins( entity npc, vector origin, float minRadius, float maxRadius, int randomCount, int desiredCount )
+{
+ array<vector> targetOrigins
+/*
+ vector angles = npc.GetAngles()
+ angles.x = 0
+ angles.z = 0
+
+ vector origin = npc.GetOrigin() + Vector( 0, 0, 1 )
+ float arc = 0
+ float dist = 200
+
+ for ( ;; )
+ {
+ if ( dist > 2000 || targetOrigins.len() >= 12 )
+ break
+
+ angles = AnglesCompose( angles, <0,arc,0> )
+ arc += 35
+ arc %= 360
+ dist += 200
+
+ vector ornull tryOrigin = TryCreateFragDroneLaunchTrajectory( npc, origin, angles, dist )
+ if ( tryOrigin == null )
+ continue
+ expect vector( tryOrigin )
+ targetOrigins.append( tryOrigin )
+ }
+*/
+ float traceFrac = TraceLineSimple( origin, origin + <0, 0, 200>, npc )
+ if ( traceFrac < 1 )
+ return targetOrigins;
+
+ array< vector > randomSpots = NavMesh_RandomPositions_LargeArea( origin, HULL_HUMAN, randomCount, minRadius, maxRadius )
+
+ int numFragDrones = 0
+ foreach( spot in randomSpots )
+ {
+ targetOrigins.append( spot )
+ numFragDrones++
+ if ( numFragDrones == desiredCount )
+ break
+ }
+
+ return targetOrigins
+}
+
+vector ornull function TryCreateFragDroneLaunchTrajectory( entity npc, vector origin, vector angles, float dist )
+{
+ vector forward = AnglesToForward( angles )
+ vector targetOrigin = origin + forward * dist
+
+ vector ornull clampedPos = NavMesh_ClampPointForHullWithExtents( targetOrigin, HULL_HUMAN, < 300, 300, 100 > )
+
+ if ( clampedPos == null )
+ return null
+
+ vector vel = GetVelocityForDestOverTime( origin, expect vector( clampedPos ), SPAWN_PROJECTILE_AIR_TIME )
+ float traceFrac = TraceLineSimple( origin, origin + vel, npc )
+ //DebugDrawLine( origin, origin + vel, 255, 0, 0, true, 5.0 )
+ if ( traceFrac >= 0.5 )
+ return clampedPos
+ return null
+}
+
+void function FragDroneDeplyAnimation( entity drone, float minDelay = 0.5, float maxDelay = 2.5 )
+{
+ Assert( !drone.ai.fragDroneArmed, "Armed drone was told to play can animation. Spawn drone with CreateFragDroneCan()" )
+ drone.EndSignal( "OnDeath" )
+
+ drone.SetInvulnerable()
+ OnThreadEnd(
+ function() : ( drone )
+ {
+ drone.ClearInvulnerable()
+ }
+ )
+
+ drone.Anim_ScriptedPlay( "sd_closed_idle" )
+ wait RandomFloatRange( minDelay, maxDelay )
+
+ #if MP
+ while ( !drone.IsInterruptable() )
+ {
+ WaitFrame()
+ }
+ #endif
+
+ drone.Anim_ScriptedPlay( "sd_closed_to_open" )
+
+ // Wait for P_drone_frag_open_flicker FX to play inside sd_closed_to_open
+ wait 0.6
+}
+
+void function LaunchSpawnerProjectile( entity npc, vector targetOrigin, int activeMinions_EntArrayID, string droneSettings = "" )
+{
+ //npc.EndSignal( "OnDeath" )
+
+ entity weapon = npc.GetOffhandWeapon( 0 )
+
+ if ( !IsValid( weapon ) )
+ return
+
+ int id = npc.LookupAttachment( "launch" )
+ vector launchPos = npc.GetAttachmentOrigin( id )
+ int team = npc.GetTeam()
+ vector launchAngles = npc.GetAngles()
+ string squadname = expect string( npc.kv.squadname )
+ vector vel = GetVelocityForDestOverTime( launchPos, targetOrigin, SPAWN_PROJECTILE_AIR_TIME )
+
+// DebugDrawLine( npc.GetOrigin() + <3,3,3>, launchPos + <3,3,3>, 255, 0, 0, true, 5.0 )
+ float armTime = SPAWN_PROJECTILE_AIR_TIME + RandomFloatRange( 1.0, 2.5 )
+ entity nade = weapon.FireWeaponGrenade( launchPos, vel, <200,0,0>, armTime, damageTypes.dissolve, damageTypes.explosive, PROJECTILE_NOT_PREDICTED, true, true )
+
+ AddToScriptManagedEntArray( activeMinions_EntArrayID, nade )
+ AddToScriptManagedEntArray( file.activeMinions_GlobalArrayIdx, nade )
+
+ nade.SetOwner( npc )
+ nade.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( nade, team, activeMinions_EntArrayID, squadname, droneSettings )
+ {
+ vector origin = nade.GetOrigin()
+ vector angles = nade.GetAngles()
+
+ vector ornull clampedPos = NavMesh_ClampPointForHullWithExtents( origin, HULL_HUMAN, < 100, 100, 100 > )
+ if ( clampedPos == null )
+ return
+
+ entity drone = CreateFragDroneCan( team, expect vector( clampedPos ), < 0, angles.y, 0 > )
+ SetSpawnOption_SquadName( drone, squadname )
+ if ( droneSettings != "" )
+ {
+ SetSpawnOption_AISettings( drone, droneSettings )
+ }
+ drone.kv.spawnflags = SF_NPC_ALLOW_SPAWN_SOLID // clamped to navmesh no need to check solid
+ DispatchSpawn( drone )
+
+ thread FragDroneDeplyAnimation( drone )
+
+ AddToScriptManagedEntArray( activeMinions_EntArrayID, drone )
+ AddToScriptManagedEntArray( file.activeMinions_GlobalArrayIdx, drone )
+ }
+ )
+
+ Grenade_Init( nade, weapon )
+
+ EmitSoundOnEntity( npc, "SpectreLauncher_AI_WpnFire" )
+ WaitForever()
+
+// wait SPAWN_PROJECTILE_AIR_TIME + SPAWN_FUSE_TIME
+}
+
+
+// Seriously don't use this unless absolutely necessary! Used for scripted moment in Reapertown.
+// Bypasses all of the tick launch rules and sends a request for launching ticks to code immediately.
+void function ForceTickLaunch( entity npc )
+{
+ SuperSpectre_LaunchFragDrone_Think( npc, file.activeMinions_GlobalArrayIdx )
+}
+
+
+/************************************************************************************************\
+######## ######## ####### ######## ####### ######## ## ## ######## ########
+## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## #### ## ## ##
+######## ######## ## ## ## ## ## ## ## ######## ######
+## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ####### ## ####### ## ## ## ########
+\************************************************************************************************/
+
+
+function SuperSpectre_WarpFall( entity ai )
+{
+ ai.EndSignal( "OnDestroy" )
+
+ vector origin = ai.GetOrigin()
+ entity mover = CreateOwnedScriptMover( ai )
+ ai.SetParent( mover, "", false, 0 )
+ ai.Hide()
+ ai.SetEfficientMode( true )
+ ai.SetInvulnerable()
+
+ WaitFrame() // give AI time to hide before moving
+
+ vector warpPos = origin + < 0, 0, 1000 >
+ mover.SetOrigin( warpPos )
+
+ #if GRUNTCHATTER_ENABLED
+ GruntChatter_TryIncomingSpawn( ai, origin )
+ #endif
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, "Titan_1P_Warpfall_Start" )
+
+ local e = {}
+ e.warpfx <- PlayFX( TURBO_WARP_FX, warpPos + < 0, 0, -104 >, mover.GetAngles() )
+ e.smokeFx <- null
+
+ OnThreadEnd(
+ function() : ( e, mover, ai )
+ {
+ if ( IsAlive( ai ) )
+ {
+ ai.ClearParent()
+ ai.SetVelocity( <0,0,0> )
+ ai.Signal( "WarpfallComplete" )
+ }
+ if ( IsValid( e.warpfx ) )
+ e.warpfx.Destroy()
+ if ( IsValid( e.smokeFx ) )
+ e.smokeFx.Destroy()
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ }
+ )
+ wait 0.5
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, "Titan_3P_Warpfall_WarpToLanding" )
+
+ wait 0.4
+
+ ai.Show()
+
+ e.smokeFx = PlayFXOnEntity( TURBO_WARP_COMPANY, ai, "", <0.0, 0.0, 152.0> )
+
+ local time = 0.2
+ mover.MoveTo( origin, time, 0, 0 )
+ wait time
+
+ ai.SetEfficientMode( false )
+ ai.ClearInvulnerable()
+
+ e.smokeFx.Destroy()
+ PlayFX( $"droppod_impact", origin )
+
+ Explosion_DamageDefSimple(
+ damagedef_reaper_fall,
+ origin,
+ ai, // attacker
+ ai, // inflictor
+ origin )
+
+ wait 0.1
+}
+
+bool function ShouldNukeOnDeath( entity ent )
+{
+ if ( IsMultiplayer() )
+ return false
+
+ return ent.Dev_GetAISettingByKeyField( "nuke_on_death" ) == 1
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_base_gametype.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_base_gametype.gnut
new file mode 100644
index 000000000..a4c6e187b
--- /dev/null
+++ b/Northstar.CustomServers/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
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_base_gametype_mp.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_base_gametype_mp.gnut
new file mode 100644
index 000000000..244d323ea
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_base_gametype_mp.gnut
@@ -0,0 +1,586 @@
+untyped
+global function BaseGametype_Init_MPSP
+global function CodeCallback_OnClientConnectionStarted
+global function CodeCallback_OnClientConnectionCompleted
+global function CodeCallback_OnClientDisconnected
+global function CodeCallback_OnPlayerRespawned
+global function CodeCallback_OnPlayerKilled
+global function DecideRespawnPlayer
+global function RespawnAsPilot
+global function RespawnAsTitan
+global function TryGameModeAnnouncement
+
+global function SetKillcamsEnabled
+global function KillcamsEnabled
+
+global function ShouldEntTakeDamage_SPMP
+global function GetTitanBuildTime
+global function TitanPlayerHotDropsIntoLevel
+
+struct {
+ bool killcamsEnabled = true
+
+ entity intermissionCamera
+ array<entity> specCams
+} file
+
+void function BaseGametype_Init_MPSP()
+{
+ AddSpawnCallback( "info_intermission", SetIntermissionCamera )
+ AddCallback_EntitiesDidLoad( SetSpecCams )
+
+ RegisterSignal( "ObserverTargetChanged" )
+ AddClientCommandCallback( "spec_next", ClientCommandCallback_spec_next )
+ AddClientCommandCallback( "spec_prev", ClientCommandCallback_spec_prev )
+ AddClientCommandCallback( "spec_mode", ClientCommandCallback_spec_mode )
+}
+
+void function SetIntermissionCamera( entity camera )
+{
+ file.intermissionCamera = camera
+}
+
+void function SetSpecCams()
+{
+ // spec cams are called spec_cam1,2,3 etc by default, so this is the easiest way to get them imo
+ int camNum = 1
+ entity lastCam = null
+ do {
+ lastCam = GetEnt( "spec_cam" + camNum++ )
+
+ if ( lastCam != null )
+ file.specCams.append( lastCam )
+ } while ( lastCam != null )
+}
+
+void function CodeCallback_OnClientConnectionStarted( entity player )
+{
+ // not a real player?
+ #if DEV
+ if ( player.GetPlayerName() == "Replay" )
+ return
+ #endif
+
+ if ( IsLobby() )
+ {
+ Lobby_OnClientConnectionStarted( player )
+ return
+ }
+
+// ScreenFade( player, 0, 0, 0, 255, 2.0, 0.5, FFADE_IN | FFADE_PURGE )
+
+ SetTargetName( player, "player" + player.entindex() )
+
+ player.p.controllableProjectiles_scriptManagedID = CreateScriptManagedEntArray()
+ player.p.npcFollowersArrayID = CreateScriptManagedEntArray()
+
+ player.s = {}
+ player.s.attackerInfo <- {}
+ player.p.clientScriptInitialized = player.IsBot()
+ player.s.inPostDeath <- null
+ player.s.respawnCount <- 0
+ player.s.respawnTime <- 0
+ player.s.lostTitanTime <- 0
+ player.s.cloakedShotsAllowed <- 0
+ player.s.startDashMeleeTime <- 0
+ player.s.respawnSelectionDone <- true // this gets set to false in postdeaththread but we need it to be true when connecting
+ player.s.waveSpawnProtection <- false
+
+ player.s.nextStatUpdateFunc <- null
+
+ player.s.activeTrapArrayId <- CreateScriptManagedEntArray()
+
+ player.s.restartBurnCardEffectOnSpawn <- false
+ player.s.replacementDropInProgress <- false
+
+ player.s.inGracePeriod <- true
+
+ // should I just add these when playing coop?
+ player.s.usedLoadoutCrate <- false
+ player.s.restockAmmoTime <- 0
+ player.s.restockAmmoCrate <- null
+
+ player.s.autoTitanLastEngageCalloutTime <- 0
+ player.s.autoTitanLastEngageCallout <- null
+ player.s.lastAIConversationTime <- {} // when was a conversation last played?
+
+ player.s.updatedPersistenceOnDisconnect <- false
+
+ player.s.lastFriendlySpawnedOn <- null
+ player.s.nextWaveSpawnTime <- 0.0
+
+ player.s.meleeSlowMoEndTime <- 0.0
+
+ player.p.connectTime = Time()
+
+ Assert( !player._entityVars )
+ InitEntityVars( player )
+
+ // Added via AddCallback_OnClientConnecting
+ foreach ( callbackFunc in svGlobal.onClientConnectingCallbacks )
+ {
+ callbackFunc( player )
+ }
+
+ printl( "Player connect started: " + player )
+
+ InitPassives( player )
+}
+
+// playerconnected
+void function CodeCallback_OnClientConnectionCompleted( entity player )
+{
+ if ( IsLobby() )
+ {
+ Lobby_OnClientConnectionCompleted( player )
+ return
+ }
+
+ player.hasConnected = true
+
+ InitMeleeAnimEventCallbacks( player )
+ ZiplineInit( player )
+
+ UpdateMinimapStatus( player )
+ UpdateMinimapStatusToOtherPlayers( player )
+ MinimapPlayerConnected( player )
+ NotifyClientsOfConnection( player, 1 )
+ PlayCurrentTeamMusicEventsOnPlayer( player )
+ SetCurrentTeamObjectiveForPlayer( player )
+
+ FinishClientScriptInitialization( player )
+
+ // Added via AddCallback_OnClientConnected
+ foreach ( callbackFunc in svGlobal.onClientConnectedCallbacks )
+ {
+ callbackFunc( player )
+ }
+
+ if ( !Flag( "PlayerDidSpawn") )
+ __PlayerDidSpawn( player )
+
+ svGlobal.levelEnt.Signal( "PlayerDidSpawn", { player = player } )
+
+ // handle spawning late joiners
+ if ( GetGameState() == eGameState.Playing )
+ {
+ if ( RespawnsEnabled() )
+ {
+ // likely temp, deffo needs some work
+ if ( Riff_SpawnAsTitan() == 1 ) // spawn as titan
+ thread RespawnAsTitan( player )
+ else // spawn as pilot
+ RespawnAsPilot( player )
+ }
+ else
+ thread PlayerBecomesSpectator( player )
+ }
+}
+
+void function CodeCallback_OnClientDisconnected( entity player, string reason )
+{
+ if ( IsLobby() )
+ {
+ player.Signal( "_disconnectedInternal" )
+ UpdateBadRepPresent()
+ return
+ }
+
+ if ( !player.hasConnected )
+ return
+
+ // Added via AddCallback_OnClientDisconnected
+ foreach ( callbackFunc in svGlobal.onClientDisconnectedCallbacks )
+ {
+ callbackFunc( player )
+ }
+
+ player.Disconnected()
+ player.p.isDisconnected = true
+ player.CleanupMPClasses()
+}
+
+void function CodeCallback_OnPlayerRespawned( entity player )
+{
+ player.Signal( "OnRespawned" ) // kill any postdeaththreads that could be running
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_YouRespawned" )
+ player.s.respawnTime = Time()
+
+ Loadouts_TryGivePilotLoadout( player )
+
+ foreach ( void functionref( entity ) callback in svGlobal.onPlayerRespawnedCallbacks )
+ callback( player )
+}
+
+void function CodeCallback_OnPlayerKilled( entity player, var damageInfo )
+{
+ PlayerOrNPCKilled( player, damageInfo )
+
+ if ( player.IsTitan() )
+ SoulDies( player.GetTitanSoul(), damageInfo ) // cleanup some titan stuff, no idea where else to put this
+
+ thread PostDeathThread_MP( player, damageInfo )
+}
+
+void function PostDeathThread_MP( entity player, var damageInfo ) // based on gametype_sp: postdeaththread_sp
+{
+ // honestly this feels jank af, it's messy and the sp code it's based off is a bit of a pain imo, needs a rewrite at some point
+ // also this likely needs an onthreadend to set a couple values
+
+ //if ( player.p.watchingPetTitanKillReplay )
+ // return
+
+ if ( player.s.inPostDeath )
+ return
+
+ float timeOfDeath = Time()
+ player.p.postDeathThreadStartTime = Time()
+
+ Assert( IsValid( player ), "Not a valid player" )
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnRespawned" )
+
+ player.p.deathOrigin = player.GetOrigin()
+ player.p.deathAngles = player.GetAngles()
+
+ player.s.inPostDeath = true
+ player.s.respawnSelectionDone = false
+
+ player.cloakedForever = false
+ player.stimmedForever = false
+ player.SetNoTarget( false )
+ player.SetNoTargetSmartAmmo( false )
+ player.ClearExtraWeaponMods()
+
+ ClearRespawnAvailable( player )
+
+ OnThreadEnd( function() : ( player )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ player.SetPredictionEnabled( true )
+ player.s.inPostDeath = false
+ })
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ int methodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ player.p.rematchOrigin = player.p.deathOrigin
+ if ( IsValid( attacker ) && methodOfDeath == eDamageSourceId.titan_execution )
+ {
+ // execution can throw you out of the map
+ player.p.rematchOrigin = attacker.GetOrigin()
+ }
+
+ player.Signal( "RodeoOver" )
+ player.ClearParent()
+
+ // do some pre-replay stuff if we're gonna do a replay
+ float replayLength = CalculateLengthOfKillReplay( player, methodOfDeath )
+ bool shouldDoReplay = Replay_IsEnabled() && KillcamsEnabled() && ShouldDoReplay( player, attacker, replayLength, methodOfDeath )
+ table replayTracker = { validTime = null }
+ if ( shouldDoReplay )
+ thread TrackDestroyTimeForReplay( attacker, replayTracker )
+
+ int damageSource = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( damageSource == eDamageSourceId.fall )
+ {
+ // this is straight up just incorrect lol, based off tf1 stuff
+
+ player.SetObserverModeStaticPosition( player.GetOrigin() )
+ player.SetObserverModeStaticAngles( player.GetVelocity() * -1 )
+
+ player.StartObserverMode( OBS_MODE_STATIC_LOCKED )
+ player.SetObserverTarget( null )
+ }
+ else
+ {
+ player.StartObserverMode( OBS_MODE_DEATHCAM )
+ if ( ShouldSetObserverTarget( attacker ) )
+ player.SetObserverTarget( attacker )
+ else
+ player.SetObserverTarget( null )
+ }
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_YouDied", attacker.GetEncodedEHandle(), GetHealthFrac( attacker ), methodOfDeath )
+
+ float deathcamLength = GetDeathCamLength( player )
+ wait deathcamLength
+
+ // use info_intermission camera after deathcam, if it exists
+ if ( file.intermissionCamera != null )
+ {
+ player.SetObserverModeStaticPosition( file.intermissionCamera.GetOrigin() )
+ player.SetObserverModeStaticAngles( file.intermissionCamera.GetAngles() )
+ player.StartObserverMode( OBS_MODE_STATIC_LOCKED )
+ player.SetObserverTarget( null )
+ }
+
+ // quick note: in cases where player.Die() is called: e.g. for round ends, player == attacker
+ if ( shouldDoReplay )
+ {
+ player.SetPredictionEnabled( false )
+
+ player.watchingKillreplayEndTime = Time() + replayLength
+ float beforeTime = GetKillReplayBeforeTime( player, methodOfDeath )
+
+ replayTracker.validTime <- null
+
+ float respawnTime = Time() - 2 // seems to get the killreplay to end around the actual kill
+ if ( "respawnTime" in attacker.s )
+ respawnTime = Time() - expect float ( attacker.s.respawnTime )
+
+ thread PlayerWatchesKillReplay( player, attacker.GetEncodedEHandle(), attacker.GetIndexForEntity(), respawnTime, timeOfDeath, beforeTime, replayTracker )
+ thread EndReplayOnTime( player, replayLength )
+ }
+
+ player.SetPlayerSettings( "spectator" ) // prevent a crash with going from titan => pilot on respawn
+
+ if ( RespawnsEnabled() )
+ {
+ // is it a good idea to do respawn code in postdeaththread? fuck if i know lol
+ float respawnDelay = max( 0, GetCurrentPlaylistVarFloat( "respawn_delay", 0.0 ) - deathcamLength )
+
+ print( "respawn delay " + respawnDelay )
+
+ UpdateNextRespawnTime( player, Time() + respawnDelay )
+ SetRespawnAvailable( player )
+
+ wait respawnDelay
+
+ player.SetPredictionEnabled( true )
+
+ player.WaitSignal( "RespawnMe" ) // set in base_gametype: ClientCommand_RespawnPlayer
+ ClearRespawnAvailable( player ) // need so the respawn icon doesn't show for like a frame on next death
+
+ if ( ( expect bool( player.GetPersistentVar( "spawnAsTitan" ) ) && IsTitanAvailable( player ) ) || Riff_SpawnAsTitan() == 1 ) // spawn as titan
+ thread RespawnAsTitan( player )
+ else // spawn as pilot
+ RespawnAsPilot( player )
+ }
+ else
+ {
+ thread PlayerBecomesSpectator( player )
+ }
+}
+
+void function EndReplayOnTime( entity player, float replayLength )
+{
+ player.EndSignal( "RespawnMe" )
+ player.EndSignal( "OnRespawned" )
+
+ wait replayLength
+ if ( IsValid( player ) && KillcamsEnabled() )
+ {
+ player.ClearReplayDelay()
+ player.ClearViewEntity()
+ player.SetPredictionEnabled( true )
+
+ player.SetObserverTarget( null )
+ }
+}
+
+void function DecideRespawnPlayer( entity player )
+{
+ // this isn't even used atm, could likely be removed if some vanilla code didn't rely on it
+
+ Assert( IsValid( player ), player + " is invalid!!" )
+ Assert( !IsAlive( player ), player + " is already alive" )
+ Assert( player.hasConnected, player + "isn't connected" )
+
+ if ( GetClassicMPMode() && GetGameState() < eGameState.Playing )
+ return // let intro functions handle spawning if we're in classicmp and not spawned yet
+
+
+}
+
+void function RespawnAsPilot( entity player, bool manualPosition = false )
+{
+ player.RespawnPlayer( FindSpawnPoint( player, false, ShouldStartSpawn( player ) && !IsFFAGame() ) )
+}
+
+void function RespawnAsTitan( entity player, bool manualPosition = false )
+{
+ player.isSpawning = true
+
+ entity spawnpoint = FindSpawnPoint( player, true, ShouldStartSpawn( player ) && !IsFFAGame() )
+
+ TitanLoadoutDef titanLoadout = GetTitanLoadoutForPlayer( player )
+
+ asset model = GetPlayerSettingsAssetForClassName( titanLoadout.setFile, "bodymodel" )
+ Attachment warpAttach = GetAttachmentAtTimeFromModel( model, "at_hotdrop_01", "offset", spawnpoint.GetOrigin(), spawnpoint.GetAngles(), 0 )
+ PlayFX( TURBO_WARP_FX, warpAttach.position, warpAttach.angle )
+
+ player.RespawnPlayer( null ) // spawn player as pilot so they get their pilot loadout on embark
+
+ entity titan = CreateAutoTitanForPlayer_FromTitanLoadout( player, titanLoadout, spawnpoint.GetOrigin(), spawnpoint.GetAngles() )
+ DispatchSpawn( titan )
+ player.SetPetTitan( null ) // prevent embark prompt from showing up
+
+ AddCinematicFlag( player, CE_FLAG_HIDE_MAIN_HUD ) // hide hud
+ player.HolsterWeapon() // hide crosshair
+
+ // do titanfall scoreevent
+ AddPlayerScore( player, "Titanfall", player )
+
+ entity camera = CreateTitanDropCamera( spawnpoint.GetAngles(), < 90, 10, 0 > )
+ camera.SetParent( titan )
+
+ // calc offset for spawnpoint angle
+ // todo this seems bad but too lazy to figure it out rn
+ //vector xyOffset = RotateAroundOrigin2D( < 44, 0, 0 >, < 0, 0, 0>, spawnpoint.GetAngles().y )
+ //xyOffset.z = 520 // < 44, 0, 520 > at 0,0,0, seems to be the offset used in tf2
+ //print( xyOffset )
+
+ vector xyOffset = RotateAroundOrigin2D( < 44, 0, 520 >, < 0, 0, 0 >, spawnpoint.GetAngles().y )
+
+ camera.SetLocalOrigin( xyOffset )
+ camera.SetLocalAngles( < camera.GetAngles().x, spawnpoint.GetAngles().y, camera.GetAngles().z > )
+ camera.Fire( "Enable", "!activator", 0, player )
+
+ waitthread TitanHotDrop( titan, "at_hotdrop_01", spawnpoint.GetOrigin(), spawnpoint.GetAngles(), player, camera ) // do hotdrop anim
+
+ camera.Fire( "Disable", "!activator", 0, player ) // stop using the camera
+ camera.Destroy()
+ RemoveCinematicFlag( player, CE_FLAG_HIDE_MAIN_HUD ) // show hud
+ player.DeployWeapon() // let them use weapons again
+ player.isSpawning = false
+
+ PilotBecomesTitan( player, titan ) // make player titan
+ titan.Destroy() // pilotbecomestitan leaves an npc titan that we need to delete
+}
+
+
+// spectator stuff
+
+void function PlayerBecomesSpectator( entity player )
+{
+ player.StartObserverMode( OBS_MODE_CHASE )
+
+ player.EndSignal( "OnRespawned" )
+ player.EndSignal( "OnDestroy" )
+
+ int targetIndex = 0
+
+ while ( true )
+ {
+ table result = player.WaitSignal( "ObserverTargetChanged" )
+
+ array<entity> targets
+
+ targets.append( file.intermissionCamera )
+ foreach( entity cam in file.specCams )
+ targets.append( cam )
+
+
+ array<entity> targetPlayers
+ if ( IsFFAGame() )
+ targetPlayers = GetPlayerArray_Alive()
+ else
+ targetPlayers = GetPlayerArrayOfTeam_Alive( player.GetTeam() )
+
+ foreach( entity player in targetPlayers )
+ targets.append( player )
+
+ if ( result.next )
+ targetIndex = ( targetIndex + 1 ) % targets.len()
+ else
+ {
+ if ( targetIndex == 0 )
+ targetIndex = ( targets.len() - 1 )
+ else
+ targetIndex--
+ }
+
+ entity target = targets[ targetIndex ]
+
+ player.StopObserverMode()
+ player.SetSpecReplayDelay( 0.0 ) // clear spectator replay
+
+ if ( target.IsPlayer() )
+ {
+ player.SetObserverTarget( target )
+ player.StartObserverMode( OBS_MODE_CHASE )
+ }
+ else
+ {
+ player.SetObserverModeStaticPosition( target.GetOrigin() )
+ player.SetObserverModeStaticAngles( target.GetAngles() )
+ player.StartObserverMode( OBS_MODE_STATIC )
+ }
+ }
+}
+
+bool function ClientCommandCallback_spec_next( entity player, array<string> args )
+{
+ if ( player.GetObserverMode() == OBS_MODE_CHASE || player.GetObserverMode() == OBS_MODE_STATIC || player.GetObserverMode() == OBS_MODE_IN_EYE )
+ player.Signal( "ObserverTargetChanged", { next = true } )
+
+ return true
+}
+
+bool function ClientCommandCallback_spec_prev( entity player, array<string> args )
+{
+ if ( player.GetObserverMode() == OBS_MODE_CHASE || player.GetObserverMode() == OBS_MODE_STATIC || player.GetObserverMode() == OBS_MODE_IN_EYE )
+ player.Signal( "ObserverTargetChanged", { next = false } )
+
+ return true
+}
+
+bool function ClientCommandCallback_spec_mode( entity player, array<string> args )
+{
+ // currently unsure how this actually gets called on client, works through console and has references in client.dll tho
+ if ( player.GetObserverMode() == OBS_MODE_CHASE )
+ {
+ // set to first person spectate
+ player.SetSpecReplayDelay( FIRST_PERSON_SPECTATOR_DELAY )
+ player.SetViewEntity( player.GetObserverTarget(), true )
+ player.StartObserverMode( OBS_MODE_IN_EYE )
+ }
+ else if ( player.GetObserverMode() == OBS_MODE_IN_EYE )
+ {
+ // set to third person spectate
+ player.SetSpecReplayDelay( 0.0 )
+ player.StartObserverMode( OBS_MODE_CHASE )
+ }
+
+ return true
+}
+
+
+void function TryGameModeAnnouncement( entity player ) // only putting this here because it's here in gametype_sp lol
+{
+ Remote_CallFunction_NonReplay( player, "ServerCallback_GameModeAnnouncement" )
+ PlayFactionDialogueToPlayer( GameMode_GetGameModeAnnouncement( GAMETYPE ), player )
+}
+
+void function SetKillcamsEnabled( bool enabled )
+{
+ file.killcamsEnabled = enabled
+}
+
+bool function KillcamsEnabled()
+{
+ return file.killcamsEnabled
+}
+
+// stuff to change later
+
+
+
+bool function ShouldEntTakeDamage_SPMP( entity ent, var damageInfo )
+{
+ return true
+}
+
+float function GetTitanBuildTime(entity player)
+{
+ return 100.0
+}
+
+void function TitanPlayerHotDropsIntoLevel( entity player )
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_battery_port.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_battery_port.gnut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_battery_port.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_bleedout.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_bleedout.gnut
new file mode 100644
index 000000000..2192b4b1e
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_bleedout.gnut
@@ -0,0 +1,403 @@
+//Bleed Out Mechanic Shared by several game modes.
+global function Bleedout_Init
+global function Bleedout_StartPlayerBleedout
+global function Bleedout_SetCallback_OnPlayerStartBleedout
+global function Bleedout_SetCallback_OnPlayerGiveFirstAid
+global function Bleedout_ShouldAIMissBleedingPlayer
+
+const asset FX_BLOODTRAIL = $"skit_blood_decal_LG"
+const float BLEEDOUT_MAX_USE_DIST2_MOD = 64 * 64
+
+struct
+{
+ table<entity,bool> isBleeding
+ table<entity, entity> IsGettingFirstAidFrom
+ table<entity,entity> lastAttacker
+ void functionref(entity) Callback_OnPlayerStartBleedout
+ void functionref(entity) Callback_OnPlayerGiveFirstAid
+ int firstAidAttemptID = 0 //The ID that identifies the first aid attempt. Used to distinguish between simultainous healing attempts on the client
+} file
+
+void function Bleedout_Init()
+{
+ RegisterSignal( "BleedOut_StopBleeding" )
+ RegisterSignal( "BleedOut_OnRevive" )
+ RegisterSignal( "BleedOut_OnStartDying" )
+ RegisterSignal( "OnContinousUseStopped" )
+
+ AddCallback_OnClientConnected( Bleedout_OnClientConnected )
+ AddCallback_OnClientDisconnected( Bleedout_OnClientDisconnected )
+
+ PrecacheParticleSystem( FX_BLOODTRAIL )
+}
+
+void function Bleedout_OnClientConnected( entity player )
+{
+ file.isBleeding[ player ] <- false
+ file.IsGettingFirstAidFrom[ player ] <- null
+ file.lastAttacker[ player ] <- svGlobal.worldspawn
+}
+
+void function Bleedout_OnClientDisconnected( entity player )
+{
+ delete file.isBleeding[ player ]
+ delete file.IsGettingFirstAidFrom[ player ]
+ delete file.lastAttacker[ player ]
+}
+
+void function Bleedout_SetCallback_OnPlayerStartBleedout( void functionref(entity) callback )
+{
+ file.Callback_OnPlayerStartBleedout = callback
+}
+
+void function Bleedout_SetCallback_OnPlayerGiveFirstAid( void functionref(entity) callback )
+{
+ file.Callback_OnPlayerGiveFirstAid = callback
+}
+
+void function Bleedout_StartPlayerBleedout( entity player, entity attacker )
+{
+ //if the player is already bleeding don't restart bleeding logic.
+ if ( file.isBleeding[ player ] )
+ return
+
+ player.Signal( "BleedOut_StopBleeding" )
+ player.Signal( "BleedOut_OnStartDying" )
+
+ file.lastAttacker[ player ] = attacker
+
+ if ( IsValid( file.Callback_OnPlayerStartBleedout ) && !file.isBleeding[ player ] )
+ file.Callback_OnPlayerStartBleedout( player )
+
+ thread BloodTrail( player )
+ thread PlayerDying( player )
+ thread EnablePlayerRes( player )
+
+ //Start selfhealing thread if enabled.
+ if ( Bleedout_GetSelfResEnabled() )
+ thread EnablePlayerSelfRes( player )
+
+ if ( Bleedout_GetDeathOnTeamBleedout() )
+ CheckForTeamBleedout( player.GetTeam() )
+}
+
+void function PlayerDying( entity player )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "BleedOut_OnRevive" )
+ player.EndSignal( "BleedOut_OnStartDying" )
+
+ float bleedoutTime = Bleedout_GetBleedoutTime()
+ bool forceHolster = Bleedout_GetForceWeaponHolster()
+
+ array<int> ids = []
+ ids.append( StatusEffect_AddEndless( player, eStatusEffect.move_slow, 0.25 ) )
+ ids.append( StatusEffect_AddEndless( player, eStatusEffect.turn_slow, 0.3 ) )
+
+ if ( bleedoutTime > 0 )
+ ids.append( StatusEffect_AddEndless( player, eStatusEffect.bleedoutDOF, 1.0 ) )
+
+ file.isBleeding[ player ] = true
+
+ player.ForceCrouch()
+ player.SetOneHandedWeaponUsageOn()
+
+ if ( forceHolster )
+ HolsterAndDisableWeapons( player )
+
+ OnThreadEnd(
+ function() : ( player, ids, forceHolster )
+ {
+ if ( IsValid( player ) )
+ {
+ foreach ( id in ids )
+ StatusEffect_Stop( player, id )
+
+ file.isBleeding[ player ] = false
+ file.lastAttacker[ player ] = svGlobal.worldspawn
+
+ player.UnforceCrouch()
+ player.SetOneHandedWeaponUsageOff()
+ //Remote_CallFunction_NonReplay( player, "ServerCallback_BLEEDOUT_PlayerRevivedDOF" )
+
+ if ( forceHolster )
+ DeployAndEnableWeapons( player )
+
+ //Hide wounded icon for wounded player's allies
+ int woundedPlayerEHandle = player.GetEncodedEHandle()
+ array<entity> teamPlayers = GetPlayerArrayOfTeam( player.GetTeam() )
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( teamPlayer == player )
+ continue
+ Remote_CallFunction_NonReplay( teamPlayer, "ServerCallback_BLEEDOUT_HideWoundedMarker", woundedPlayerEHandle )
+ }
+ }
+ }
+ )
+
+ //if ( bleedoutTime > 0 )
+ // StatusEffect_AddTimed( player, eStatusEffect.bleedoutDOF, 1.0, bleedoutTime, 0.0 )
+ //Remote_CallFunction_NonReplay( player, "ServerCallback_BLEEDOUT_StartDyingDOF", bleedoutTime )
+
+ //Show wounded icon for wounded player's allies
+ int woundedPlayerEHandle = player.GetEncodedEHandle()
+ array<entity> teamPlayers = GetPlayerArrayOfTeam( player.GetTeam() )
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( teamPlayer == player )
+ continue
+
+ Remote_CallFunction_NonReplay( teamPlayer, "ServerCallback_BLEEDOUT_ShowWoundedMarker", woundedPlayerEHandle, Time(), Time() + bleedoutTime )
+ }
+
+ if ( bleedoutTime > 0 )
+ wait bleedoutTime
+ else
+ WaitForever()
+
+ PlayerDiesFromBleedout( player, file.lastAttacker[ player ] )
+}
+
+void function EnablePlayerRes( entity player )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "BleedOut_OnStartDying" )
+ player.EndSignal( "BleedOut_OnRevive" )
+
+ Highlight_SetFriendlyHighlight( player, "interact_object_los_line" )
+
+ if ( IsPilotEliminationBased() )
+ SetPlayerEliminated( player )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ {
+ player.UnsetUsable()
+ Highlight_ClearFriendlyHighlight( player )
+ }
+ }
+ )
+
+ while ( true )
+ {
+ //If the player is not currently being treated or is self healing. (Team healing should always override self-healing)
+ if ( !IsPlayerGettingFirstAid( player ) || IsPlayerSelfHealing( player ) )
+ {
+ player.SetUsableByGroup( "friendlies pilot" )
+ player.SetUsePrompts( "#BLEEDOUT_USE_TEAMMATE_RES", "#BLEEDOUT_USE_TEAMMATE_RES_PC" )
+
+ entity playerHealer = expect entity ( player.WaitSignal( "OnPlayerUse" ).player )
+ player.UnsetUsable()
+
+ //Player can only res other players if they are not bleeding out themselves.
+ if ( !file.isBleeding[ playerHealer ] && ( !IsPlayerGettingFirstAid( player ) || IsPlayerSelfHealing( player ) ) )
+ waitthread PlayerAttemptRes( playerHealer, player )
+ }
+ else
+ {
+ WaitFrame()
+ }
+ }
+}
+
+void function EnablePlayerSelfRes( entity player )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "BleedOut_OnStartDying" )
+ player.EndSignal( "BleedOut_OnRevive" )
+
+ while ( true )
+ {
+ if ( !IsPlayerGettingFirstAid( player ) )
+ MessageToPlayer( player, eEventNotifications.BLEEDOUT_SelfHealPrompt )
+
+ if ( player.UseButtonPressed() && !IsPlayerGettingFirstAid( player ) )
+ {
+ MessageToPlayer( player, eEventNotifications.Clear )
+ waitthread PlayerAttemptRes( player, player )
+ }
+
+ WaitFrame()
+ }
+}
+
+void function PlayerAttemptRes( entity playerHealer, entity playerToRes )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ playerToRes.EndSignal( "OnDeath" )
+ playerHealer.EndSignal( "OnDeath" )
+ playerHealer.EndSignal( "OnContinousUseStopped" )
+
+ HolsterAndDisableWeapons( playerHealer )
+
+ playerHealer.MovementDisable()
+ playerToRes.MovementDisable()
+
+ float firstAidTime = playerHealer == playerToRes ? Bleedout_GetFirstAidTimeSelf() : Bleedout_GetFirstAidTime()
+ float firstAidHealPercent = Bleedout_GetFirstAidHealPercent()
+
+ float endTime = Time() + firstAidTime
+
+ int playerEHandle = playerToRes.GetEncodedEHandle()
+ int healerEHandle = playerHealer.GetEncodedEHandle()
+ int attemptID = GetNewFirstAidAttemptID()
+
+ Remote_CallFunction_NonReplay( playerToRes, "ServerCallback_BLEEDOUT_StartFirstAidProgressBar", endTime, playerEHandle, healerEHandle, attemptID )
+ Remote_CallFunction_NonReplay( playerHealer, "ServerCallback_BLEEDOUT_StartFirstAidProgressBar", endTime, playerEHandle, healerEHandle, attemptID )
+ file.IsGettingFirstAidFrom[ playerToRes ] = playerHealer
+
+ OnThreadEnd(
+ function() : ( playerHealer, playerToRes, attemptID )
+ {
+ if ( IsValid( playerHealer ) )
+ {
+ DeployAndEnableWeapons( playerHealer )
+ playerHealer.MovementEnable()
+ Remote_CallFunction_NonReplay( playerHealer, "ServerCallback_BLEEDOUT_StopFirstAidProgressBar", attemptID )
+ }
+
+ if ( IsValid( playerToRes ) )
+ {
+ file.IsGettingFirstAidFrom[ playerToRes ] = null
+ playerToRes.MovementEnable()
+ Remote_CallFunction_NonReplay( playerToRes, "ServerCallback_BLEEDOUT_StopFirstAidProgressBar", attemptID )
+ }
+ }
+ )
+
+ waitthread TrackContinuousUse( playerHealer, playerToRes, firstAidTime, true )
+
+ //Heal player health
+ playerToRes.SetHealth( playerToRes.GetMaxHealth() * firstAidHealPercent )
+ file.isBleeding[ playerToRes ] = false
+ file.lastAttacker[ playerToRes ] = svGlobal.worldspawn
+ if ( IsPilotEliminationBased() )
+ ClearPlayerEliminated( playerToRes )
+
+ if ( IsValid( file.Callback_OnPlayerGiveFirstAid ) )
+ {
+ //Do not run this callback if player is self healing.
+ if ( playerHealer != playerToRes )
+ file.Callback_OnPlayerGiveFirstAid( playerHealer )
+ }
+
+ playerToRes.Signal( "BleedOut_OnRevive" )
+
+}
+
+void function BloodTrail( entity player )
+{
+ player.EndSignal( "BleedOut_StopBleeding" )
+ player.EndSignal( "BleedOut_OnRevive" )
+ player.EndSignal( "OnDeath")
+
+ while ( true )
+ {
+ float interval = RandomFloatRange( 0.25, 0.5 )
+ PlayFXOnEntity( FX_BLOODTRAIL, player )
+ wait interval
+ }
+}
+
+void function PlayerDiesFromBleedout( entity player, entity attacker )
+{
+ if ( IsValid( attacker ) )
+ {
+ player.Die( attacker, attacker, { damageSourceId = eDamageSourceId.bleedout } )
+ //player.BecomeRagdoll( Vector(0,0,0), false )
+ }
+ else
+ {
+ player.Die( svGlobal.worldspawn, svGlobal.worldspawn, { damageSourceId = eDamageSourceId.bleedout } )
+ //player.BecomeRagdoll( Vector(0,0,0), false )
+ }
+
+
+}
+
+//This function checks to see if all players on a team are dead or bleeding out.
+//If all the players are dead/bleeding out, it kills the surviving team players.
+void function CheckForTeamBleedout( int team )
+{
+ array<entity> teamPlayers = GetPlayerArrayOfTeam( team )
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( IsAlive( teamPlayer ) && !file.isBleeding[ teamPlayer ] )
+ return
+ }
+
+ //All players on team are bleeding out
+ foreach ( entity teamPlayer in teamPlayers )
+ {
+ if ( IsAlive( teamPlayer ) )
+ PlayerDiesFromBleedout( teamPlayer, file.lastAttacker[ teamPlayer ] )
+ }
+}
+
+bool function Bleedout_ShouldAIMissBleedingPlayer( entity player )
+{
+ //If the player is not bleeding
+ if ( !file.isBleeding[ player ] )
+ return false
+
+ //If the bleedout settings don't affect AI accuracy.
+ if ( !Bleedout_ShouldAIMissPlayer() )
+ return false
+
+ return true
+}
+
+bool function IsPlayerGettingFirstAid( entity player )
+{
+ return file.IsGettingFirstAidFrom[ player ] != null
+}
+
+bool function IsPlayerSelfHealing( entity player )
+{
+ return file.IsGettingFirstAidFrom[ player ] == player
+}
+
+//////////////
+//Utilities
+//////////////
+void function TrackContinuousUse( entity player, entity useTarget, float useTime, bool doRequireUseButtonHeld )
+{
+ player.EndSignal( "OnDeath" )
+ useTarget.EndSignal( "OnDeath" )
+ useTarget.EndSignal( "OnDestroy" )
+
+ table result = {}
+ result.success <- false
+
+ float maxDist2 = DistanceSqr( player.GetOrigin(), useTarget.GetOrigin() ) + BLEEDOUT_MAX_USE_DIST2_MOD
+
+ OnThreadEnd
+ (
+ function() : ( player, result )
+ {
+ if ( !result.success )
+ {
+ player.Signal( "OnContinousUseStopped" )
+ }
+ }
+ )
+
+ float startTime = Time()
+ while ( Time() < startTime + useTime && (!doRequireUseButtonHeld || player.UseButtonPressed()) && !player.IsPhaseShifted() && DistanceSqr( player.GetOrigin(), useTarget.GetOrigin() ) <= maxDist2 )
+ WaitFrame()
+
+ if ( ( !doRequireUseButtonHeld || player.UseButtonPressed() ) && DistanceSqr( player.GetOrigin(), useTarget.GetOrigin() ) <= maxDist2 )
+ result.success = true
+}
+
+int function GetNewFirstAidAttemptID()
+{
+ file.firstAidAttemptID += 1
+ return file.firstAidAttemptID
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_challenges.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_challenges.gnut
new file mode 100644
index 000000000..466a50425
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_challenges.gnut
@@ -0,0 +1,6 @@
+global function InitChallenges
+
+void function InitChallenges()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_changemap.nut b/Northstar.CustomServers/scripts/vscripts/mp/_changemap.nut
new file mode 100644
index 000000000..4d6dc3937
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_changemap.nut
@@ -0,0 +1,24 @@
+global function CodeCallback_MatchIsOver
+
+
+void function CodeCallback_MatchIsOver()
+{
+ foreach ( entity player in GetPlayerArray() )
+ SavePdataForEntityIndex( player.GetPlayerIndex() )
+
+ if ( !IsPrivateMatch() && IsMatchmakingServer() )
+ SetUIVar( level, "putPlayerInMatchmakingAfterDelay", true )
+ else
+ SetUIVar( level, "putPlayerInMatchmakingAfterDelay", false )
+
+ if ( GetCurrentPlaylistVarInt( "return_to_private_lobby", 0 ) == 1 ) // set in _private_lobby.gnut, temp lol
+ {
+ SetCurrentPlaylist( "private_match" ) // needed for private lobby to load
+ ServerCommand( "changelevel mp_lobby" )
+ }
+
+#if DEV
+ if ( !IsMatchmakingServer() )
+ GameRules_ChangeMap( "mp_lobby", GAMETYPE )
+#endif // #if DEV
+}
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp.nut b/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp.nut
new file mode 100644
index 000000000..d6ac8f55a
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp.nut
@@ -0,0 +1,67 @@
+untyped
+global function ClassicMp_Init
+global function ClassicMP_TryDefaultIntroSetup // called in mp_sh_init
+global function ClassicMP_SetCustomIntro
+global function ClassicMP_OnIntroStarted
+global function ClassicMP_OnIntroFinished
+global function ClassicMP_GetIntroLength
+global function GetClassicMPMode
+
+struct {
+ void functionref() introSetupFunc
+ float introLength
+} file
+
+void function ClassicMp_Init()
+{
+ // literally nothing to do here atm lol
+}
+
+void function ClassicMP_TryDefaultIntroSetup()
+{
+ if ( file.introSetupFunc == null )
+ {
+ if ( IsFFAGame() )
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, NOINTRO_INTRO_LENGTH )
+ else
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultDropshipIntro_Setup, DROPSHIP_INTRO_LENGTH )
+ }
+
+ thread DelayedDoDefaultIntroSetup()
+}
+
+void function DelayedDoDefaultIntroSetup()
+{
+ // wait a frame for CodeCallback_MapInit to run which generally sets custom intros
+ WaitFrame()
+ file.introSetupFunc()
+}
+
+void function ClassicMP_SetCustomIntro( void functionref() setupFunc, float introLength )
+{
+ file.introSetupFunc = setupFunc
+ file.introLength = introLength
+}
+
+void function ClassicMP_OnIntroStarted()
+{
+ print( "started intro!" )
+ SetServerVar( "gameStartTime", Time() + file.introLength )
+ SetServerVar( "roundStartTime", Time() + file.introLength )
+}
+
+void function ClassicMP_OnIntroFinished()
+{
+ print( "intro finished!" )
+ SetGameState( eGameState.Playing )
+}
+
+float function ClassicMP_GetIntroLength()
+{
+ return file.introLength
+}
+
+bool function GetClassicMPMode()
+{
+ return GetCurrentPlaylistVarInt( "classic_mp", 1 ) == 1
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut
new file mode 100644
index 000000000..20455c696
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut
@@ -0,0 +1,230 @@
+untyped
+global function ClassicMP_DefaultDropshipIntro_Setup
+
+const array<string> DROPSHIP_IDLE_ANIMS = [ "Classic_MP_flyin_exit_playerA_idle",
+ "Classic_MP_flyin_exit_playerB_idle",
+ "Classic_MP_flyin_exit_playerC_idle",
+ "Classic_MP_flyin_exit_playerD_idle" ]
+
+const array<string> DROPSHIP_IDLE_ANIMS_POV = [ "Classic_MP_flyin_exit_povA_idle",
+ "Classic_MP_flyin_exit_povB_idle",
+ "Classic_MP_flyin_exit_povC_idle",
+ "Classic_MP_flyin_exit_povD_idle" ]
+
+const array<string> DROPSHIP_JUMP_ANIMS = [ "Classic_MP_flyin_exit_playerA_jump",
+ "Classic_MP_flyin_exit_playerB_jump",
+ "Classic_MP_flyin_exit_playerC_jump",
+ "Classic_MP_flyin_exit_playerD_jump" ]
+
+const array<string> DROPSHIP_JUMP_ANIMS_POV = [ "Classic_MP_flyin_exit_povA_jump",
+ "Classic_MP_flyin_exit_povB_jump",
+ "Classic_MP_flyin_exit_povC_jump",
+ "Classic_MP_flyin_exit_povD_jump" ]
+
+const array<int> DROPSHIP_ANIMS_YAW = [ -18, 8, 8, -16 ]
+
+global const float DROPSHIP_INTRO_LENGTH = 15.0 // TODO tweak this
+
+struct IntroDropship
+{
+ entity dropship
+
+ int playersInDropship
+ entity[4] players
+}
+
+struct {
+ IntroDropship[2] militiaDropships
+ IntroDropship[2] imcDropships
+
+ float introStartTime
+ int numPlayersInIntro
+} file
+
+
+void function ClassicMP_DefaultDropshipIntro_Setup()
+{
+ AddCallback_OnClientConnected( DropshipIntro_OnClientConnected )
+ AddCallback_OnClientDisconnected( DropshipIntro_OnClientDisconnected )
+
+ AddCallback_GameStateEnter( eGameState.Prematch, OnPrematchStart )
+}
+
+void function DropshipIntro_OnClientConnected( entity player )
+{
+ // find the player's team's dropships
+ IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships
+
+ // find a dropship with an empty slot
+ foreach ( IntroDropship dropship in teamDropships )
+ if ( dropship.playersInDropship < 4 )
+ // we've found a valid dropship
+ // find an empty player slot
+ for ( int i = 0; i < dropship.players.len(); i++ )
+ if ( dropship.players[ i ] == null ) // empty slot
+ {
+ dropship.players[ i ] = player
+ dropship.playersInDropship++
+
+ // spawn player into intro if we're already doing intro
+ if ( GetGameState() == eGameState.Prematch )
+ thread SpawnPlayerIntoDropship( player )
+
+ return
+ }
+
+}
+
+void function DropshipIntro_OnClientDisconnected( entity player )
+{
+ // find the player's dropship
+ IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships
+
+ // find the player
+ foreach ( IntroDropship dropship in teamDropships )
+ for ( int i = 0; i < dropship.players.len(); i++ )
+ if ( dropship.players[ i ] == player )
+ {
+ // we've found the player, remove them
+ dropship.players[ i ] = null
+ dropship.playersInDropship--
+
+ return
+ }
+}
+
+void function OnPrematchStart()
+{
+ ClassicMP_OnIntroStarted()
+
+ print( "starting dropship intro!" )
+ file.introStartTime = Time()
+
+ // spawn dropships
+ array<entity> dropshipSpawns = GetEntArrayByClass_Expensive( "info_spawnpoint_dropship_start" )
+ foreach ( entity dropshipSpawn in dropshipSpawns )
+ {
+ if ( GameModeRemove( dropshipSpawn ) )
+ continue
+
+ // todo: possibly make this only spawn dropships if we've got enough players to need them
+ int createTeam = GetServerVar( "switchedSides" ) != 1 ? dropshipSpawn.GetTeam() : GetOtherTeam( dropshipSpawn.GetTeam() )
+ IntroDropship[2] teamDropships = createTeam == TEAM_MILITIA ? file.militiaDropships : file.imcDropships
+ int dropshipIndex = !IsValid( teamDropships[ 0 ].dropship ) ? 0 : 1
+
+ // create entity
+ entity dropship = CreateDropship( createTeam, dropshipSpawn.GetOrigin(), dropshipSpawn.GetAngles() )
+
+ teamDropships[ dropshipIndex ].dropship = dropship
+ AddAnimEvent( dropship, "dropship_warpout", WarpoutEffect )
+
+ DispatchSpawn( dropship )
+
+ // have to do this after dispatch otherwise it won't work for some reason
+ dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" )
+ // could also use $"models/vehicle/goblin_dropship/goblin_dropship_hero.mdl", unsure which
+
+ thread PlayAnim( dropship, "dropship_classic_mp_flyin" )
+ }
+
+ foreach ( entity player in GetPlayerArray() )
+ thread SpawnPlayerIntoDropship( player )
+}
+
+void function SpawnPlayerIntoDropship( entity player )
+{
+ if ( IsAlive( player ) )
+ player.Die() // kill them so we don't have any issues respawning them later
+
+ WaitFrame()
+
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "Disconnected" )
+
+ file.numPlayersInIntro++
+
+ // find the player's dropship and seat
+ IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships
+ IntroDropship playerDropship
+ int playerDropshipIndex
+ foreach ( IntroDropship dropship in teamDropships )
+ for ( int i = 0; i < dropship.players.len(); i++ )
+ if ( dropship.players[ i ] == player )
+ {
+ playerDropship = dropship
+ playerDropshipIndex = i
+
+ break
+ }
+
+ // figure out what anims we're using for idle
+ string idleAnim = DROPSHIP_IDLE_ANIMS[ playerDropshipIndex ]
+ string idleAnimPov = DROPSHIP_IDLE_ANIMS_POV[ playerDropshipIndex ]
+
+ FirstPersonSequenceStruct idleSequence
+ idleSequence.firstPersonAnim = idleAnimPov
+ idleSequence.thirdPersonAnim = idleAnim
+ idleSequence.attachment = "ORIGIN"
+ idleSequence.teleport = true
+ idleSequence.viewConeFunction = ViewConeRampFree
+ idleSequence.hideProxy = true
+ idleSequence.setInitialTime = Time() - file.introStartTime
+
+ // respawn player and holster their weapons so they aren't out
+ player.RespawnPlayer( null )
+ player.DisableWeaponViewModel()
+
+ // hide hud and fade screen out from black
+ AddCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+ ScreenFadeFromBlack( player, 0.5, 0.5 )
+ // faction leaders are done clientside, spawn them here
+ Remote_CallFunction_NonReplay( player, "ServerCallback_SpawnFactionCommanderInDropship", playerDropship.dropship.GetEncodedEHandle(), file.introStartTime )
+ thread FirstPersonSequence( idleSequence, player, playerDropship.dropship )
+
+ // wait until the anim is done
+ WaittillAnimDone( player ) // unsure if this is the best way to do this
+ // todo: possibly rework this to actually get the time the idle anim takes and start the starttime of the jump sequence for very late joiners using that
+
+ // honestly go rewrite alot of this too it's messy
+
+ // figure out what anims we're using for jump
+ string jumpAnim = DROPSHIP_JUMP_ANIMS[ playerDropshipIndex ]
+ string jumpAnimPov = DROPSHIP_JUMP_ANIMS_POV[ playerDropshipIndex ]
+
+ FirstPersonSequenceStruct jumpSequence
+ jumpSequence.firstPersonAnim = jumpAnimPov
+ jumpSequence.thirdPersonAnim = jumpAnim
+ jumpSequence.attachment = "ORIGIN"
+ //jumpSequence.setInitialTime = Time() - ( file.introStartTime + player.GetSequenceDuration( idleAnim ) )
+ jumpSequence.setInitialTime = Time() - ( file.introStartTime + 10.9 ) // pretty sure you should do this with GetScriptedAnimEventCycleFrac?
+ // idk unsure how to use that, all i know is getsequenceduration > the length it actually should be
+
+ thread FirstPersonSequence( jumpSequence, player, playerDropship.dropship )
+ WaittillAnimDone( player )
+
+ // unparent player and their camera from the dropship
+ player.ClearParent()
+ ClearPlayerAnimViewEntity( player )
+
+ file.numPlayersInIntro--
+ if ( file.numPlayersInIntro == 0 )
+ ClassicMP_OnIntroFinished() // set intro as finished
+
+ // wait for intro timer to be fully done
+ wait( Time() - ( file.introStartTime + DROPSHIP_INTRO_LENGTH ) )
+ player.MovementDisable() // disable all movement but let them look around still
+ player.ConsumeDoubleJump() // movementdisable doesn't prevent double jumps
+
+ // wait for player to hit the ground
+ while ( !player.IsOnGround() && !player.IsWallRunning() && !player.IsWallHanging() ) // todo this needs tweaking
+ WaitFrame()
+
+ // show weapon viewmodel and hud and let them move again
+ player.MovementEnable()
+ player.EnableWeaponViewModel()
+ RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+
+ if ( GetServerVar( "switchedSides" ) != 1 )
+ TryGameModeAnnouncement( player )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp_no_intro.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp_no_intro.gnut
new file mode 100644
index 000000000..50e9e9a07
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_classic_mp_no_intro.gnut
@@ -0,0 +1,46 @@
+untyped
+
+global function ClassicMP_DefaultNoIntro_Setup
+global const float NOINTRO_INTRO_LENGTH = 10.0
+
+void function ClassicMP_DefaultNoIntro_Setup()
+{
+ AddCallback_OnClientConnected( ClassicMP_DefaultNoIntro_SpawnPlayer )
+ AddCallback_GameStateEnter( eGameState.Prematch, ClassicMP_DefaultNoIntro_Start )
+}
+
+void function ClassicMP_DefaultNoIntro_Start()
+{
+ ClassicMP_OnIntroStarted()
+
+ foreach ( entity player in GetPlayerArray() )
+ thread ClassicMP_DefaultNoIntro_SpawnPlayer( player )
+
+ wait NOINTRO_INTRO_LENGTH
+
+ foreach ( entity player in GetPlayerArray() )
+ {
+ player.UnfreezeControlsOnServer()
+ RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+ TryGameModeAnnouncement( player )
+ }
+
+ ClassicMP_OnIntroFinished()
+}
+
+void function ClassicMP_DefaultNoIntro_SpawnPlayer( entity player )
+{
+ if ( GetGameState() != eGameState.Prematch )
+ return
+
+ if ( IsAlive( player ) )
+ {
+ player.Die()
+ WaitFrame()
+ }
+
+ RespawnAsPilot( player )
+ player.FreezeControlsOnServer()
+ AddCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+ ScreenFadeFromBlack( player, 0.5, 0.5 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_codecallbacks.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_codecallbacks.gnut
new file mode 100644
index 000000000..2e5651422
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_codecallbacks.gnut
@@ -0,0 +1,999 @@
+untyped
+
+
+global function CodeCallback_Init
+global function CodeCallback_DamagePlayerOrNPC
+global function GameModeRulesShouldGiveTimerCredit
+global function SetGameModeRulesShouldGiveTimerCredit
+global function SetGameModeRulesEarnMeterOnDamage
+global function GetDamageOrigin
+global function CodeCallBack_ShouldTriggerSniperCam
+global function CodeCallback_ForceAIMissPlayer
+global function CodeCallback_OnTouchHealthKit
+global function CodeCallback_OnPlayerGrappled
+global function CodeCallback_OnProjectileGrappled
+global function DamageInfo_ScaleDamage
+global function CodeCallback_CheckPassThroughAddsMods
+global function SetTitanMeterGainScale
+
+#if MP
+global function CodeCallback_OnServerAnimEvent
+#endif
+
+struct AccumulatedDamageData
+{
+ float accumulatedDamage
+ float lastDamageTime
+}
+
+struct
+{
+ float titanMeterGainScale = 0.0001
+ bool functionref( entity, entity, var ) ShouldGiveTimerCreditGameModeRules
+ void functionref( entity, entity, TitanDamage, float ) earnMeterOnDamageGameModeRulesCallback
+
+ table<entity, AccumulatedDamageData> playerAccumulatedDamageData
+} file
+
+void function CodeCallback_Init()
+{
+ file.ShouldGiveTimerCreditGameModeRules = ShouldGiveTimerCredit_Default
+ file.earnMeterOnDamageGameModeRulesCallback = GameModeRulesEarnMeterOnDamage_Default
+ RegisterSignal( "DamagedPlayerOrNPC" )
+ RegisterSignal( "UpdateAccumulatedDamageAfterDelay" )
+
+ AddCallback_OnClientConnected( OnClientConnected )
+}
+
+void function OnClientConnected( entity player )
+{
+ AccumulatedDamageData damageData
+ file.playerAccumulatedDamageData[player] <- damageData
+}
+
+// TODO: Get an equivalent callback happening on the client, so we can stop using ServerCallback_PlayerTookDamage which is always out of date to some degree.
+void function CodeCallback_DamagePlayerOrNPC( entity ent, var damageInfo )
+{
+ bool entIsPlayer = ent.IsPlayer()
+ bool entIsTitan = ent.IsTitan()
+ bool entIsNPC = ent.IsNPC()
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+
+ bool attackerIsPlayer = false
+ bool attackerIsTitan = false
+ bool attackerIsNPC = false
+
+ if ( IsValid( attacker ) )
+ {
+ attackerIsPlayer = attacker.IsPlayer()
+ attackerIsTitan = attacker.IsTitan()
+ attackerIsNPC = attacker.IsNPC()
+ }
+
+ // Set damage source correctly when npc grunts or titans try to melee us
+ if ( attackerIsNPC && DamageInfo_GetCustomDamageType( damageInfo ) & DF_MELEE )
+ {
+ if ( IsValid( attacker ) )
+ {
+ if ( attackerIsTitan )
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.auto_titan_melee )
+ }
+ else if ( IsSpectre( attacker ) )
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.spectre_melee )
+ }
+ else if ( IsProwler( attacker ) )
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.prowler_melee )
+ }
+ else if ( IsSuperSpectre( attacker ) )
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.super_spectre_melee )
+ }
+ else
+ {
+ DamageInfo_SetDamageSourceIdentifier( damageInfo, eDamageSourceId.grunt_melee )
+ }
+ }
+ }
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( "CodeCallback_DamagePlayerOrNPC ent:", ent )
+ printt( " Attacker:", DamageInfo_GetAttacker( damageInfo ) )
+ printt( " Inflictor:", DamageInfo_GetInflictor( damageInfo ) )
+ printt( " Distance:", DamageInfo_GetDistFromAttackOrigin( damageInfo ) )
+ printt( " Original damage:", DamageInfo_GetDamage( damageInfo ) )
+ printt( " Hitbox:", DamageInfo_GetHitBox( damageInfo ) )
+ int sourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ printt( " SourceID:", sourceID )
+ if ( sourceID == -1 )
+ printt( " SourceID: From Code (npc melee, etc)" )
+ else
+ printt( " SourceID:", GetObitFromDamageSourceID( sourceID ) )
+
+ PrintDamageFlags( DamageInfo_GetCustomDamageType( damageInfo ) )
+ #endif
+
+ if ( !ScriptCallback_ShouldEntTakeDamage( ent, damageInfo ) )
+ {
+ // EMP triggers on damage, but in some cases players are invlunerable (embark, disembark, etc...)
+ if ( entIsPlayer && DamageInfo_GetDamageSourceIdentifier( damageInfo ) in level._empForcedCallbacks )
+ {
+ if ( ShouldPlayEMPEffectEvenWhenDamageIsZero( ent, attacker ) )
+ EMP_DamagedPlayerOrNPC( ent, damageInfo )
+ }
+
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ if ( ( IsAirDrone( ent ) ) && ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) in level._empForcedCallbacks ) )
+ {
+ EMP_DamagedPlayerOrNPC( ent, damageInfo )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == damagedef_titan_step )
+ HandleFootstepDamage( ent, damageInfo )
+
+ // HACK helps trap/grenade weapons do damage to the correct entities (player who deployed it as well as the team opposite his)
+ if ( IsValid( inflictor ) && "originalOwner" in inflictor.s )
+ {
+ local ogOwner = inflictor.s.originalOwner
+ if ( IsValid( ogOwner ) )
+ {
+ // if the victim is the guy who damaged the trap, and he is not the ogOwner...
+ if ( ent == attacker && ent != ogOwner )
+ {
+ // HACK to do this legit we need DamageInfo_SetAttacker( damageInfo )
+ // victim should take damage from the original owner instead of the satchel attacker so he gets a kill credit
+ ent.TakeDamage( DamageInfo_GetDamage( damageInfo ), ogOwner, inflictor, { weapon = DamageInfo_GetWeapon( damageInfo ), origin = DamageInfo_GetDamagePosition( damageInfo ), force = DamageInfo_GetDamageForce( damageInfo ), scriptType = DamageInfo_GetCustomDamageType( damageInfo ), damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo ) } )
+
+ // now zero out the normal damage and return
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ }
+ }
+
+ if ( IsValid( inflictor ) )
+ {
+ if ( inflictor.IsProjectile() && entIsPlayer )
+ {
+ if ( inflictor.proj.damageScale != 1.0 )
+ {
+ DamageInfo_ScaleDamage( damageInfo, inflictor.proj.damageScale )
+ }
+
+ // Don't take damage from projectiles created before you where spawned.
+ if ( inflictor.GetProjectileCreationTime() < ent.s.respawnTime && ( Time() - ent.s.respawnTime ) < 2.0 )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ }
+
+ if ( inflictor.e.onlyDamageEntitiesOnce == true || inflictor.e.onlyDamageEntitiesOncePerTick == true )
+ {
+ Assert( !inflictor.e.damagedEntities.contains(ent) )
+ inflictor.e.damagedEntities.append( ent )
+ }
+ }
+
+ // Round damage to nearest full value
+ DamageInfo_SetDamage( damageInfo, floor( DamageInfo_GetDamage( damageInfo ) + 0.5 ) )
+ if ( DamageInfo_GetDamage( damageInfo ) <= 0 )
+ return
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " rounded damage amount:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ HandleLocationBasedDamage( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after location based damage:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ //PROTO Defensive AI Chip. Ideally less invisible gameplay, but something that can combo with other chips.
+ if ( ent.IsTitan() && entIsNPC )
+ {
+ entity soul = ent.GetTitanSoul()
+ if ( IsValid( soul ) && SoulHasPassive( soul, ePassives.PAS_GUARDIAN_CHIP ) )
+ {
+ DamageInfo_SetDamage( damageInfo, DamageInfo_GetDamage( damageInfo ) * 0.8 )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( "After guardian chip :", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+ }
+ }
+
+ RunClassDamageCallbacks( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after class damage callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+
+ // use AddDamageByCallback( "classname", function ) to registed functions
+ if ( IsValid( attacker ) )
+ {
+ if ( attackerIsTitan )
+ {
+ entity soul = attacker.GetTitanSoul()
+ if ( IsValid( soul ) )
+ {
+ float damageAmpScale = 1.0 + StatusEffect_Get( soul, eStatusEffect.titan_damage_amp )
+ if ( damageAmpScale != 1.0 )
+ DamageInfo_ScaleDamage( damageInfo, damageAmpScale )
+ }
+ }
+
+ string attackerClassName = attacker.GetClassName()
+ if ( attackerClassName in svGlobal.damageByCallbacks )
+ {
+ foreach ( callbackFunc in svGlobal.damageByCallbacks[attackerClassName] )
+ {
+ callbackFunc( ent, damageInfo )
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+ }
+ }
+ }
+
+ float damageMultiplier = 1.0 + StatusEffect_Get( ent, eStatusEffect.damage_received_multiplier )
+ if ( damageMultiplier != 1.0 )
+ DamageInfo_ScaleDamage( damageInfo, damageMultiplier )
+
+ // Added via AddEntityCallback_OnDamaged
+ foreach ( callbackFunc in ent.e.entDamageCallbacks )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after AddEntityCallback_OnDamaged callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ // use AddDamageCallbackSourceID( "classname", function ) to registed functions
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( damageSourceId in shGlobal.damageSourceIdCallbacks )
+ {
+ foreach ( callbackFunc in shGlobal.damageSourceIdCallbacks[ damageSourceId ] )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+ }
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after damageSourceID callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+
+ RunClassDamageFinalCallbacks( ent, damageInfo )
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " after class damage final callbacks:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ DamageInfo_AddDamageFlags( damageInfo, DAMAGEFLAG_NOPAIN )
+
+ float savedDamage = DamageInfo_GetDamage( damageInfo )
+
+ TitanDamage titanDamage
+ if ( entIsPlayer )
+ {
+ PlayerTookDamage( ent, damageInfo, attacker, inflictor, damageSourceId, titanDamage )
+ if ( DamageInfo_GetDamage( damageInfo ) == 0 && entIsTitan )
+ {
+ EarnMeterDamageConversion( damageInfo, attacker, ent, 0, titanDamage )
+ return
+ }
+
+ if ( attackerIsPlayer )
+ PlayerDamageFeedback( ent, damageInfo )
+ savedDamage = DamageInfo_GetDamage( damageInfo )
+
+ if ( !entIsTitan )
+ ent.SetCloakFlicker( 0.5, 0.65 )
+ }
+ else
+ {
+ Assert( entIsNPC )
+ bool clearedDamage
+ if ( ent.ai.buddhaMode )
+ {
+ float currentDamage = DamageInfo_GetDamage( damageInfo )
+ int remainingHealth = ent.GetHealth()
+
+ if ( currentDamage >= remainingHealth - ( DOOMED_MIN_HEALTH + 1 ) )
+ {
+ currentDamage = max( remainingHealth - ( DOOMED_MIN_HEALTH + 1 ), 0 )
+ DamageInfo_SetDamage( damageInfo, currentDamage )
+ clearedDamage = currentDamage == 0
+ }
+ }
+
+ if ( !clearedDamage )
+ {
+ if ( entIsTitan )
+ {
+ Titan_NPCTookDamage( ent, damageInfo, titanDamage )
+ savedDamage = DamageInfo_GetDamage( damageInfo )
+ }
+ else
+ {
+ Generic_NPCTookDamage( ent, damageInfo, titanDamage )
+ }
+ }
+
+ if ( attackerIsPlayer )
+ PlayerDamageFeedback( ent, damageInfo )
+ }
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " After player damage mod:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ if ( titanDamage.shieldDamage > 0 )
+ printt( " Shield Damage:", titanDamage.shieldDamage )
+ #endif
+
+ // Added via AddEntityCallback_OnPostDamaged
+ foreach ( callbackFunc in ent.e.entPostDamageCallbacks )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+
+ UpdateLastDamageTime( ent )
+
+ //pain sounds _base_gametype.nut, death sounds in _death_package.nut
+ UpdateDamageState( ent, damageInfo )
+ HandlePainSounds( ent, damageInfo )
+
+ UpdateAttackerInfo( ent, attacker, savedDamage )
+
+ if ( !(DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS) )
+ {
+ if ( attackerIsPlayer )
+ {
+ if ( entIsTitan )
+ {
+ PlayerDealtTitanDamage( attacker, ent, savedDamage, damageInfo )
+
+ entity entSoul = ent.GetTitanSoul()
+ if ( attacker.p.currentTargetPlayerOrSoul_Ent != entSoul )
+ {
+ attacker.p.currentTargetPlayerOrSoul_Ent = ent.GetTitanSoul()
+
+ TitanVO_TellPlayersThatAreAlsoFightingThisTarget( attacker, entSoul )
+ }
+ attacker.p.currentTargetPlayerOrSoul_LastHitTime = Time()
+ }
+ else if ( entIsPlayer )
+ {
+ attacker.p.currentTargetPlayerOrSoul_Ent = ent
+ attacker.p.currentTargetPlayerOrSoul_LastHitTime = Time()
+ }
+ }
+ }
+
+ EarnMeterDamageConversion( damageInfo, attacker, ent, savedDamage, titanDamage )
+
+ if ( entIsTitan )
+ {
+ TitanDamageFlinch( ent, damageInfo )
+
+ if ( TitanDamageRewardsTitanCoreTime() && entIsPlayer && attacker.GetTeam() != ent.GetTeam() )
+ AddCreditToTitanCoreBuilderForTitanDamageReceived( ent, savedDamage )
+ }
+
+ if ( entIsPlayer && !entIsTitan )
+ PilotDamageFlinch( ent, damageInfo )
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " final damage done:", DamageInfo_GetDamage( damageInfo ) )
+ printt( " health: " + ent.GetHealth() )
+ #endif
+
+ RunClassPostDamageCallbacks( ent, damageInfo )
+
+ #if SERVER && MP
+ Stats_OnPlayerDidDamage( ent, damageInfo )
+ PIN_DamageDone( attacker, ent, DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ attacker.Signal( "DamagedPlayerOrNPC" )
+}
+
+void function EarnMeterDamageConversion( var damageInfo, entity attacker, entity ent, float savedDamage, TitanDamage titanDamage )
+{
+ if ( !(DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS) )
+ {
+ bool shouldGiveTimerCredit = file.ShouldGiveTimerCreditGameModeRules( attacker, ent, damageInfo )
+ if ( attacker.IsPlayer() )
+ {
+ float titanSpawnDelay = GetTitanBuildTime( attacker )
+ float timerCredit = 0.0
+
+ if ( shouldGiveTimerCredit )
+ {
+ file.earnMeterOnDamageGameModeRulesCallback( attacker, ent, titanDamage, savedDamage )
+
+ // Timer Credit seems unused. Need to investigate if all DecrementBuildTimer functions are worthless.
+ if ( titanSpawnDelay && IsAlive( ent ) && GetCurrentPlaylistVarInt( "titan_build_credit_enabled", 1 ) == 1 )
+ {
+ if ( ent.IsTitan() )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "titan_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( attacker, SFLAG_HUNTER_TITAN ) )
+ timerCredit *= 2.0
+ }
+ else
+ {
+ if ( ent.IsPlayer() )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "player_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( attacker, SFLAG_HUNTER_PILOT ) )
+ timerCredit *= 2.5
+ }
+ else
+ {
+ if ( IsGrunt( ent ) )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "ai_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( attacker, SFLAG_HUNTER_GRUNT ) )
+ timerCredit *= 2.5
+ }
+ else
+ if ( IsSpectre( ent ) )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( attacker, SFLAG_HUNTER_SPECTRE ) )
+ timerCredit *= 2.5
+ }
+ else
+ if ( IsTurret( ent ) )
+ {
+
+ timerCredit = GetCurrentPlaylistVarFloat( "megaturret_kill_credit", 0.5 )
+ //No 2x burn card for shooting mega turret
+ }
+ #if HAS_EVAC
+ else
+ if ( IsEvacDropship( ent ) )
+ {
+ timerCredit = GetCurrentPlaylistVarFloat( "evac_dropship_kill_credit", 0.5 )
+ }
+ #endif
+ }
+ }
+
+ float dealtDamage = min( ent.GetHealth(), (savedDamage + titanDamage.shieldDamage) )
+ timerCredit = timerCredit * (dealtDamage / ent.GetMaxHealth().tofloat())
+ }
+
+ if ( IsPilot( attacker ) && PlayerHasPassive( attacker, ePassives.PAS_AT_HUNTER ) )
+ timerCredit *= 1.1
+
+ if ( timerCredit && (!TitanDamageRewardsTitanCoreTime() || !attacker.IsTitan() ) )
+ DecrementBuildTimer( attacker, timerCredit )
+ }
+ }
+
+ if ( shouldGiveTimerCredit //Primary Check
+ && TitanDamageRewardsTitanCoreTime() //Playlist var check
+ && ent.IsTitan()
+ && attacker.IsTitan()
+ && attacker.GetTeam() != ent.GetTeam()
+ && !attacker.ContextAction_IsMeleeExecution() // Some melee executions deal A LOT of damage
+ )
+ AddCreditToTitanCoreBuilderForTitanDamageInflicted( attacker, savedDamage + titanDamage.shieldDamage )
+ }
+}
+
+
+bool function ShouldUseNonTitanHeavyArmorDamageScale( entity victim )
+{
+ if ( (victim.GetArmorType() != ARMOR_TYPE_HEAVY) )
+ return false
+
+ if ( victim.IsTitan() )
+ return false
+
+ if ( IsDropship( victim ) )
+ return false
+
+ return true
+}
+
+void function GameModeRulesEarnMeterOnDamage_Default( entity attacker, entity victim, TitanDamage titanDamage, float savedDamage )
+{
+ #if MP
+ if ( victim.IsTitan() && !attacker.IsTitan() && !IsValid( attacker.GetPetTitan() ) )
+ {
+ float damage = min( victim.GetHealth(), (savedDamage + titanDamage.shieldDamage) )
+ float meterAmount = damage * file.titanMeterGainScale
+ if ( PlayerHasPassive( attacker, ePassives.PAS_AT_HUNTER ) )
+ meterAmount *= 1.1
+ PlayerEarnMeter_AddOwnedFrac( attacker, meterAmount )
+
+ AccumulatedDamageData damageData = file.playerAccumulatedDamageData[attacker]
+ damageData.lastDamageTime = Time()
+ damageData.accumulatedDamage += meterAmount
+
+ if ( damageData.accumulatedDamage >= 0.01 )
+ {
+ attacker.Signal( "UpdateAccumulatedDamageAfterDelay" )
+ AddPlayerScore( attacker, "DamageTitan", null, "", int( damageData.accumulatedDamage * 100 ) )
+ damageData.accumulatedDamage = 0
+ }
+ else
+ {
+ thread UpdateAccumulatedDamageAfterDelay( attacker )
+ }
+ }
+ #endif
+}
+
+void function SetTitanMeterGainScale( float scalar )
+{
+ file.titanMeterGainScale = scalar
+}
+
+#if MP
+void function UpdateAccumulatedDamageAfterDelay( entity attacker )
+{
+ attacker.EndSignal( "OnDeath" )
+ attacker.Signal( "UpdateAccumulatedDamageAfterDelay" )
+ attacker.EndSignal( "UpdateAccumulatedDamageAfterDelay" )
+
+ wait 0.25
+
+ AccumulatedDamageData damageData = file.playerAccumulatedDamageData[attacker]
+
+ if ( damageData.accumulatedDamage == 0 )
+ return
+
+ AddPlayerScore( attacker, "DamageTitan", null, "", int( max( damageData.accumulatedDamage * 100, 1 ) ) )
+ damageData.accumulatedDamage = 0
+}
+#endif
+
+void function SetGameModeRulesEarnMeterOnDamage( void functionref( entity, entity, TitanDamage, float ) rules )
+{
+ file.earnMeterOnDamageGameModeRulesCallback = rules
+}
+
+bool function ShouldGiveTimerCredit_Default( entity player, entity victim, var damageInfo )
+{
+ if ( player == victim )
+ return false
+
+ if ( player.IsTitan() && !IsCoreAvailable( player ) )
+ return false
+
+ if ( GAMETYPE == FREE_AGENCY && !player.IsTitan() )
+ return false
+
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ switch ( damageSourceID )
+ {
+ case eDamageSourceId.mp_titancore_flame_wave:
+ case eDamageSourceId.mp_titancore_flame_wave_secondary:
+ case eDamageSourceId.mp_titancore_salvo_core:
+ case damagedef_titan_fall:
+ case damagedef_nuclear_core:
+ return false
+ }
+
+ return true
+}
+
+bool function GameModeRulesShouldGiveTimerCredit( entity player, entity victim, var damageInfo )
+{
+ return file.ShouldGiveTimerCreditGameModeRules( player, victim, damageInfo )
+}
+
+void function SetGameModeRulesShouldGiveTimerCredit( bool functionref( entity, entity, var ) rules )
+{
+ file.ShouldGiveTimerCreditGameModeRules = rules
+}
+
+function TitanDamageFlinch( entity ent, damageInfo )
+{
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ if ( TitanStagger( ent, damageInfo ) )
+ return
+
+ if ( DamageInfo_GetDamage( damageInfo ) >= TITAN_ADDITIVE_FLINCH_DAMAGE_THRESHOLD )
+ AddFlinch( ent, damageInfo )
+}
+
+function PilotDamageFlinch( entity ent, damageInfo )
+{
+ //if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ // return
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+ if ( damage >= 5 )
+ AddFlinch( ent, damageInfo )
+}
+
+vector function GetDamageOrigin( damageInfo, entity victim = null )
+{
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+
+ if ( inflictor == svGlobal.worldspawn )
+ return DamageInfo_GetDamagePosition( damageInfo )
+
+ vector damageOrigin = IsValid( inflictor ) ? inflictor.GetOrigin() : DamageInfo_GetDamagePosition( damageInfo )
+
+ switch ( damageSourceId )
+ {
+ case eDamageSourceId.mp_weapon_satchel:
+ case eDamageSourceId.mp_weapon_proximity_mine:
+ case eDamageSourceId.mp_titanweapon_arc_pylon:
+ break
+
+ case damagedef_nuclear_core:
+ case eDamageSourceId.mp_titanability_smoke:
+ //if ( IsValid( victim ) && victim.IsPlayer() && IsValid( victim.GetTitanSoulBeingRodeoed() ) )
+ {
+ damageOrigin += (RandomVecInDome( Vector( 0, 0, -1 ) ) * 300.0)
+ damageOrigin += Vector( 0, 0, 128 )
+ }
+ break
+
+ case eDamageSourceId.switchback_trap:
+ if ( IsValid( victim ) && victim.IsPlayer() )
+ damageOrigin = victim.EyePosition() + (RandomVecInDome( Vector( 0, 0, -1 ) ) * 300.0)
+ break
+
+ default:
+ if ( DamageInfo_GetAttacker( damageInfo ) )
+ {
+ inflictor = DamageInfo_GetAttacker( damageInfo )
+ damageOrigin = inflictor.GetWorldSpaceCenter()
+ }
+ break
+ }
+
+ return damageOrigin
+}
+
+/*
+function TrackDPS( ent )
+{
+ ent.s.dpsTracking <- {}
+ ent.s.dpsTracking.damage <- 0
+
+ local startTime = Time()
+
+ ent.WaitSignal( "Doomed" )
+
+ local duration = Time() - startTime
+
+ printt( "DPS:", ent.s.dpsTracking.damage / duration, duration )
+
+ delete ent.s.dpsTracking
+}
+
+function UpdateDPS( ent, damageInfo )
+{
+ if ( GetDoomedState( ent ) )
+ return
+
+ if ( !( "dpsTracking" in ent.s ) )
+ thread TrackDPS( ent )
+
+ ent.s.dpsTracking.damage += DamageInfo_GetDamage( damageInfo )
+}
+*/
+
+
+void function PlayerTookDamage( entity player, var damageInfo, entity attacker, entity inflictor, int damageSourceId, TitanDamage titanDamage )
+{
+ int hitBox = DamageInfo_GetHitBox( damageInfo )
+
+ bool critHit = false
+
+ if ( CritWeaponInDamageInfo( damageInfo ) )
+ critHit = IsCriticalHit( attacker, player, hitBox, DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) )
+
+ if ( critHit )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+
+ local eModSourceID = null
+ foreach ( mod in weaponMods )
+ {
+ local modSourceID = GetModSourceID( mod )
+ if ( modSourceID != null && modSourceID in modNameStrings )
+ eModSourceID = modSourceID
+ }
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == eDamageSourceId.fall )
+ DamageInfo_SetForceKill( damageInfo, true )
+
+ bool isTitan = player.IsTitan()
+
+ if ( isTitan )
+ Titan_PlayerTookDamage( player, damageInfo, attacker, critHit, titanDamage )
+ else
+ Wallrun_PlayerTookDamage( player, damageInfo, attacker )
+
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ bool isKillShot = (damageAmount >= player.GetHealth())
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ if ( isKillShot )
+ damageType = (damageType | DF_KILLSHOT)
+
+ if ( isTitan && (DamageInfo_GetDamage( damageInfo ) == 0) )
+ {
+ if ( titanDamage.doomedNow ) // to make kill card come up for you even if you have auto-eject. In Titan_PlayerTookDamage we set damage to 0 if you have Auto-Eject and are doomed
+ TellClientPlayerTookDamage( player, damageInfo, attacker, eModSourceID, damageType, damageSourceId, titanDamage )
+ }
+
+ vector attackerOrigin = Vector( 0, 0, 0 )
+ if ( IsValid( attacker ) )
+ attackerOrigin = attacker.GetOrigin()
+
+ if ( IsAlive( player ) )
+ {
+ float storeTime = MAX_DAMAGE_HISTORY_TIME
+ entity storeEnt
+ if ( isTitan )
+ {
+ storeEnt = player.GetTitanSoul()
+ }
+ else
+ {
+ storeEnt = player
+ if ( IsSingleplayer() )
+ storeTime = 30.0
+ }
+
+ StoreDamageHistoryAndUpdate( storeEnt, storeTime, DamageInfo_GetDamage( damageInfo ), attackerOrigin, damageType, damageSourceId, attacker, weaponMods )
+ }
+
+ if ( !(DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS) )
+ TellClientPlayerTookDamage( player, damageInfo, attacker, eModSourceID, damageType, damageSourceId, titanDamage )
+}
+
+function TellClientPlayerTookDamage( entity player, damageInfo, entity attacker, eModSourceID, int damageType, int damageSourceId, TitanDamage titanDamage )
+{
+ if ( !player.hasConnected )
+ return
+
+ local attackerEHandle = IsValid( attacker ) ? attacker.GetEncodedEHandle() : null
+ local weaponEHandle = IsValid( DamageInfo_GetWeapon( damageInfo ) ) ? DamageInfo_GetWeapon( damageInfo ).GetEncodedEHandle() : null
+ local damageOrigin = GetDamageOrigin( damageInfo, player )
+
+ if ( player.IsTitan() )
+ Remote_CallFunction_Replay( player, "ServerCallback_TitanTookDamage", DamageInfo_GetDamage( damageInfo ), damageOrigin.x, damageOrigin.y, damageOrigin.z, damageType, damageSourceId, attackerEHandle, eModSourceID, titanDamage.doomedNow, titanDamage.doomedDamage )
+ else
+ Remote_CallFunction_Replay( player, "ServerCallback_PilotTookDamage", DamageInfo_GetDamage( damageInfo ), damageOrigin.x, damageOrigin.y, damageOrigin.z, damageType, damageSourceId, attackerEHandle, eModSourceID )
+}
+
+// This only handles damage events. Whizbys can still cause snipercam to trigger without passing through this check.
+function CodeCallBack_ShouldTriggerSniperCam( damageInfo )
+{
+ switch ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) )
+ {
+ case damagedef_titan_step:
+ case eDamageSourceId.super_electric_smoke_screen:
+ return false
+ }
+
+ return true
+}
+
+bool function CodeCallback_ForceAIMissPlayer( entity npc, entity player )
+{
+ return SPMP_Callback_ForceAIMissPlayer( npc, player )
+}
+
+bool function CodeCallback_OnTouchHealthKit( entity player, entity ent )
+{
+ string entityClassName = ent.GetClassName()
+
+ Assert( entityClassName in svGlobal.onTouchHealthKitCallbacks )
+
+ array<bool functionref( entity player, entity healthpack )> callbackFuncs = svGlobal.onTouchHealthKitCallbacks[ entityClassName ]
+ foreach ( callbackFunc in callbackFuncs )
+ {
+ bool result = callbackFunc( player, ent )
+ if ( result )
+ return result
+ }
+
+ return false
+}
+
+bool function ShouldPlayEMPEffectEvenWhenDamageIsZero( entity ent, entity attacker )
+{
+ if ( ent.IsTitan() && IsTitanWithinBubbleShield( ent ) )
+ return false
+
+ if ( !IsValid( attacker ) )
+ return true
+
+ if ( attacker.GetTeam() != ent.GetTeam() )
+ return true
+
+ return false
+}
+
+void function CodeCallback_OnPlayerGrappled( entity player, entity victim )
+{
+ if ( victim.GetTeam() != player.GetTeam() )
+ {
+ if ( victim.p.lastGrappledTime + TITAN_GRAPPLE_DEBOUNCE_TIME < Time() )
+ {
+ if ( player.IsTitan() )
+ {
+ victim.TakeDamage( TITAN_GRAPPLE_DAMAGE, player, player, { origin = victim.EyePosition(), scriptType = DF_GIB, damageSourceId = eDamageSourceId.titan_grapple } )
+
+ if ( victim.IsTitan() )
+ {
+ entity soul = victim.GetTitanSoul()
+ if ( soul == null )
+ soul = victim
+
+ float fadeTime = 0.5
+ StatusEffect_AddTimed( soul, eStatusEffect.dodge_speed_slow, 0.75, 0.9 + fadeTime, fadeTime )
+ StatusEffect_AddTimed( soul, eStatusEffect.move_slow, 0.75, 0.9 + fadeTime, fadeTime )
+ }
+ }
+
+ if ( victim.IsPlayer() )
+ {
+ if ( player.IsTitan() )
+ MessageToPlayer( victim, eEventNotifications.Grapple_WasGrappled_ByTitan )
+ else
+ MessageToPlayer( victim, eEventNotifications.Grapple_WasGrappled_ByPilot )
+ }
+ }
+
+ victim.p.lastGrappledTime = Time()
+ }
+}
+
+void function CodeCallback_OnProjectileGrappled( entity player, entity projectile )
+{
+
+}
+
+void function DamageInfo_ScaleDamage( var damageInfo, float scalar )
+{
+ DamageInfo_SetDamage( damageInfo, DamageInfo_GetDamage( damageInfo ) * scalar )
+}
+
+string function CodeCallback_CheckPassThroughAddsMods( entity player, entity hitEnt, string currWeaponName )
+{
+ if ( !IsValid( player ) )
+ return ""
+
+ if ( StatusEffect_Get( hitEnt, eStatusEffect.pass_through_amps_weapon ) > 0 )
+ {
+ array<string> mods = GetWeaponBurnMods( currWeaponName )
+ if ( mods.len() > 0 )
+ return mods[0]
+ }
+ return ""
+}
+
+void function Generic_NPCTookDamage( entity npc, damageInfo, TitanDamage titanDamage )
+{
+ Assert( !npc.IsTitan() )
+ Assert( DamageInfo_GetDamage( damageInfo ) > 0 )
+ Assert( IsAlive( npc ) )
+
+ bool critHit = false
+ if ( CritWeaponInDamageInfo( damageInfo ) )
+ critHit = IsCriticalHit( DamageInfo_GetAttacker( damageInfo ), npc, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) )
+
+ if ( critHit )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+
+ titanDamage.shieldDamage = NPCShieldHealthUpdate( npc, damageInfo, critHit )
+}
+
+
+int function NPCShieldHealthUpdate( entity npc, damageInfo, bool critHit )
+{
+ if ( npc.GetShieldHealth() <= 0 )
+ return 0
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == damagedef_suicide )
+ return 0
+
+ if ( DamageInfo_GetForceKill( damageInfo ) )
+ {
+ npc.SetShieldHealth( 0 )
+ return 0
+ }
+ else if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_BYPASS_SHIELD )
+ {
+ return 0
+ }
+
+ DamageInfo_AddCustomDamageType( damageInfo, DF_SHIELD_DAMAGE )
+ return int( ShieldModifyDamage( npc, damageInfo ) )
+}
+
+#if MP
+// taken from sp/sh_sp_dialogue.gnut, with some sp-exclusive stuff removed
+
+// called by code when an animation does { event AE_SV_VSCRIPT_CALLBACK FrameNumber "some string" }
+// and by a script function OnFootstep, apparently.
+void function CodeCallback_OnServerAnimEvent( entity ent, string eventName )
+{
+ PerfStart( PerfIndexServer.CB_OnServerAnimEvent )
+ if ( HasAnimEvent( ent, eventName ) )
+ thread RunAnimEventCallbacks( ent, eventName )
+
+ if ( eventName in svGlobal.globalAnimEventCallbacks )
+ {
+ thread svGlobal.globalAnimEventCallbacks[ eventName ]( ent )
+ PerfEnd( PerfIndexServer.CB_OnServerAnimEvent )
+ return
+ }
+
+
+ // couldn't find this eventName on the ent or the global anim events,
+ // so try breaking it down. If we didn't find it, it means
+ // script needs to handle the event, even if it is just to
+ // do nothing with it
+
+ array<string> tokens = split( eventName, ":" )
+ string tokenName = tokens[0]
+
+ switch ( tokenName )
+ {
+ case "worldsound":
+ GlobalAnimEventWithStringParameter_WorldSound( ent, tokens[1] )
+ break
+
+ case "signal":
+ SendSignalFromTokens( ent, tokens )
+ break
+
+ case "flagset":
+ GlobalAnimEventWithStringParameter_FlagSet( ent, tokens[1] )
+ break
+
+ //case "dialogue":
+ // // Make sure that animation triggered dialogue uses the correct priority and skips the queue
+ // string name = tokens[1]
+ // Assert( file.registeredDialogIDs.find( name ) >= 0, "Dialogue line " + name + " is not registered" )
+ // int aliasID = file.registeredDialogIDs.find( name )
+ // DialogueData data = file.registeredDialog[ aliasID ]
+ // Assert( data.priority == PRIORITY_NO_QUEUE, "Dialogue " + name + " triggered via qc must use PRIORITY_NO_QUEUE" )
+ // thread PlayDialogue( name, ent )
+ // break
+
+ case "fireViperSalvo":
+ int value = tokens[1].tointeger()
+ ent.Signal( "fireSalvo", { num = value } )
+ break
+
+ //case "conversation":
+ // thread PlayerConversation( tokens[1], GetPlayerArray()[0], ent )
+ // break
+ }
+
+ PerfEnd( PerfIndexServer.CB_OnServerAnimEvent )
+}
+#endif // #if MP \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_dropship_spawn_common.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_dropship_spawn_common.gnut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_dropship_spawn_common.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_gamestate.nut b/Northstar.CustomServers/scripts/vscripts/mp/_gamestate.nut
new file mode 100644
index 000000000..603c38fa2
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_gamestate.nut
@@ -0,0 +1,144 @@
+untyped
+
+global function GameState_Init
+global function InitGameState
+
+global function SetRoundBased
+global function SetCustomIntroLength
+
+global function SetGetDifficultyFunc
+global function GetDifficultyLevel
+
+//********************************************************************************************
+// Game State
+//********************************************************************************************
+global const PREMATCH_TIMER_INTRO_DEFAULT = 46
+global const PREMATCH_TIMER_NO_INTRO = 7 //shows 5 when fade from black
+global const CLEAR_PLAYERS_BUFFER = 2.0
+
+global const ENDROUND_FREEZE = 0
+global const ENDROUND_MOVEONLY = 1
+global const ENDROUND_FREE = 3
+
+global const NO_DETERMINED_WINNING_TEAM_YET = -1
+
+struct
+{
+ int functionref() difficultyFunc
+} file
+
+global enum eWinReason
+{
+ DEFAULT,
+ SCORE_LIMIT,
+ TIME_LIMIT,
+ ELIMINATION
+}
+
+
+function GameState_Init()
+{
+ FlagInit( "GamePlaying" )
+ FlagInit( "DisableTimeLimit" )
+ FlagInit( "DisableScoreLimit" )
+ FlagInit( "AnnounceWinnerEnabled", true )
+ FlagInit( "AnnounceProgressEnabled", true )
+ FlagInit( "DefendersWinDraw" )
+
+ RegisterSignal( "RoundEnd" )
+ RegisterSignal( "GameEnd" )
+ RegisterSignal( "GameStateChanged" )
+ RegisterSignal( "CatchUpFallBehindVO" )
+ RegisterSignal( "ClearedPlayers" )
+
+
+ level.devForcedWin <- false //For dev purposes only. Used to check if we forced a win through dev command
+ level.devForcedTimeLimit <- false
+
+ level.lastTimeLeftSeconds <- null
+
+ level.lastScoreSwapVOTime <- null
+
+ level.nextMatchProgressAnnouncementLevel <- MATCH_PROGRESS_EARLY //When we make a matchProgressAnnouncement, this variable is set
+
+ level.endOfRoundPlayerState <- ENDROUND_FREEZE
+
+ level._swapGameStateOnNextFrame <- false
+ level.clearedPlayers <- false
+
+ level.customEpilogueDuration <- null
+
+ level.lastTeamTitans <- {}
+ level.lastTeamTitans[TEAM_IMC] <- null
+ level.lastTeamTitans[TEAM_MILITIA] <- null
+ level.lastTeamPilots <- {}
+ level.lastTeamPilots[TEAM_IMC] <- null
+ level.lastTeamPilots[TEAM_MILITIA] <- null
+
+ level.firstTitanfall <- false
+
+ level.lastPlayingEmptyTeamCheck <- 0
+
+ level.doneWaitingForPlayersTimeout <- 0
+
+ level.attackDefendBased <- false
+
+ level.roundBasedUsingTeamScore <- false
+
+ level.roundBasedTeamScoreNoReset <- false
+
+ level.customIntroLength <- null
+
+ level.sendingPlayersAway <- false
+
+ level.forceNoMoreRounds <- false
+
+ // prevents ties... need an option to disable in the future
+ level.firstToScoreLimit <- TEAM_UNASSIGNED
+ level.allowPointsOverLimit <- false
+
+ file.difficultyFunc = DefaultDifficultyFunc
+
+ #if MP
+ AddCallback_EntitiesDidLoad( GameState_EntitiesDidLoad )
+ #endif
+}
+
+
+int function DefaultDifficultyFunc()
+{
+ return 0
+}
+
+void function SetGetDifficultyFunc( int functionref() difficultyFunc )
+{
+ Assert( file.difficultyFunc == DefaultDifficultyFunc )
+
+ file.difficultyFunc = difficultyFunc
+}
+
+
+// This function is meant to init stuff that _gamestate uses, as opposed
+// to stuff that any particular gamestate like Playing uses
+function InitGameState()
+{
+ #if MP
+ PIN_GameStart()
+ #endif
+}
+
+function SetRoundBased( state )
+{
+ level.nv.roundBased = state
+}
+
+function SetCustomIntroLength( time )
+{
+ level.customIntroLength = time
+}
+
+int function GetDifficultyLevel()
+{
+ return file.difficultyFunc()
+}
+
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/scripts/vscripts/mp/_gamestate_mp.nut
new file mode 100644
index 000000000..2fa24e9da
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_gamestate_mp.nut
@@ -0,0 +1,707 @@
+untyped
+
+global function PIN_GameStart
+global function SetGameState
+global function GameState_EntitiesDidLoad
+global function WaittillGameStateOrHigher
+
+global function SetShouldUsePickLoadoutScreen
+global function SetSwitchSidesBased
+global function SetSuddenDeathBased
+global function SetShouldUseRoundWinningKillReplay
+global function SetRoundWinningKillReplayKillClasses
+global function SetRoundWinningKillReplayAttacker
+global function SetWinner
+global function SetTimeoutWinnerDecisionFunc
+global function AddTeamScore
+
+global function GameState_GetTimeLimitOverride
+global function IsRoundBasedGameOver
+global function ShouldRunEvac
+global function GiveTitanToPlayer
+global function GetTimeLimit_ForGameMode
+
+struct {
+ // used for togglable parts of gamestate
+ bool usePickLoadoutScreen
+ bool switchSidesBased
+ bool suddenDeathBased
+ int functionref() timeoutWinnerDecisionFunc
+
+ // for waitingforplayers
+ int numPlayersFullyConnected
+
+ bool hasSwitchedSides
+
+ int announceRoundWinnerWinningSubstr
+ int announceRoundWinnerLosingSubstr
+
+ bool roundWinningKillReplayTrackPilotKills = true
+ bool roundWinningKillReplayTrackTitanKills = false
+
+ entity roundWinningKillReplayVictim
+ entity roundWinningKillReplayAttacker
+ int roundWinningKillReplayMethodOfDeath
+ float roundWinningKillReplayTimeOfDeath
+ float roundWinningKillReplayHealthFrac
+} file
+
+void function PIN_GameStart()
+{
+ // todo: using the pin telemetry function here is weird and was done veeery early on before i knew how this all worked, should use a different one
+
+ // called from InitGameState
+ //FlagInit( "ReadyToStartMatch" )
+
+ SetServerVar( "switchedSides", 0 )
+ SetServerVar( "winningTeam", -1 )
+
+ AddCallback_GameStateEnter( eGameState.WaitingForCustomStart, GameStateEnter_WaitingForCustomStart )
+ AddCallback_GameStateEnter( eGameState.WaitingForPlayers, GameStateEnter_WaitingForPlayers )
+ AddCallback_OnClientConnected( WaitingForPlayers_ClientConnected )
+ AddCallback_OnClientDisconnected( WaitingForPlayers_ClientDisconnected )
+
+ AddCallback_GameStateEnter( eGameState.PickLoadout, GameStateEnter_PickLoadout )
+ AddCallback_GameStateEnter( eGameState.Prematch, GameStateEnter_Prematch )
+ AddCallback_GameStateEnter( eGameState.Playing, GameStateEnter_Playing )
+ AddCallback_GameStateEnter( eGameState.WinnerDetermined, GameStateEnter_WinnerDetermined )
+ AddCallback_GameStateEnter( eGameState.SwitchingSides, GameStateEnter_SwitchingSides )
+ AddCallback_GameStateEnter( eGameState.SuddenDeath, GameStateEnter_SuddenDeath )
+ AddCallback_GameStateEnter( eGameState.Postmatch, GameStateEnter_Postmatch )
+
+ AddCallback_OnClientConnected( SetSkyCam ) // had no idea where to put this lol
+ AddCallback_OnPlayerKilled( OnPlayerKilled )
+ AddDeathCallback( "npc_titan", OnTitanKilled )
+}
+
+void function SetGameState( int newState )
+{
+ SetServerVar( "gameStateChangeTime", Time() )
+ SetServerVar( "gameState", newState )
+ svGlobal.levelEnt.Signal( "GameStateChanged" )
+
+ // added in AddCallback_GameStateEnter
+ foreach ( callbackFunc in svGlobal.gameStateEnterCallbacks[ newState ] )
+ callbackFunc()
+}
+
+void function GameState_EntitiesDidLoad()
+{
+ // nothing of importance to put here, this is referenced in _gamestate though so need it
+}
+
+void function WaittillGameStateOrHigher( int gameState )
+{
+ while ( GetGameState() < gameState )
+ svGlobal.levelEnt.WaitSignal( "GameStateChanged" )
+}
+
+
+// logic for individual gamestates:
+
+
+// eGameState.WaitingForCustomStart
+void function GameStateEnter_WaitingForCustomStart()
+{
+ // unused in release, comments indicate this was supposed to be used for an e3 demo
+ // perhaps games in this demo were manually started by an employee? no clue really
+}
+
+
+// eGameState.WaitingForPlayers
+void function GameStateEnter_WaitingForPlayers()
+{
+ foreach ( entity player in GetPlayerArray() )
+ WaitingForPlayers_ClientConnected( player )
+
+ thread WaitForPlayers( GetPendingClientsCount() + file.numPlayersFullyConnected ) // like 90% sure there should be a way to get number of loading clients on server but idk it
+}
+
+void function WaitForPlayers( int wantedNum )
+{
+ // note: atm if someone disconnects as this happens the game will just wait forever
+ print( "WaitForPlayers(): " + wantedNum + " players" )
+ float endTime = Time() + 120.0
+
+ while ( endTime > Time() )
+ {
+ if ( file.numPlayersFullyConnected >= wantedNum )
+ break
+
+ WaitFrame()
+ }
+
+ print( "done waiting!" )
+
+ wait 1.0 // bit nicer
+
+ if ( file.usePickLoadoutScreen )
+ SetGameState( eGameState.PickLoadout )
+ else
+ SetGameState( eGameState.Prematch )
+}
+
+void function WaitingForPlayers_ClientConnected( entity player )
+{
+ if ( GetGameState() == eGameState.WaitingForPlayers )
+ ScreenFadeToBlackForever( player, 0.0 )
+
+ file.numPlayersFullyConnected++
+}
+
+void function WaitingForPlayers_ClientDisconnected( entity player )
+{
+ file.numPlayersFullyConnected--
+}
+
+
+// eGameState.PickLoadout
+void function GameStateEnter_PickLoadout()
+{
+ thread GameStateEnter_PickLoadout_Threaded()
+}
+
+void function GameStateEnter_PickLoadout_Threaded()
+{
+ float pickloadoutLength = 20.0 // may need tweaking
+ SetServerVar( "minPickLoadOutTime", Time() + pickloadoutLength )
+
+ // titan selection menu can change minPickLoadOutTime so we need to wait manually until we hit the time
+ while ( Time() < GetServerVar( "minPickLoadOutTime" ) )
+ WaitFrame()
+
+ SetGameState( eGameState.Prematch )
+}
+
+
+// eGameState.Prematch
+void function GameStateEnter_Prematch()
+{
+ int timeLimit = GameMode_GetTimeLimit( GAMETYPE ) * 60
+ if ( file.switchSidesBased )
+ timeLimit /= 2 // endtime is half of total per side
+
+ SetServerVar( "gameEndTime", Time() + timeLimit + ClassicMP_GetIntroLength() )
+ SetServerVar( "roundEndTime", Time() + ClassicMP_GetIntroLength() + GameMode_GetRoundTimeLimit( GAMETYPE ) * 60 )
+}
+
+
+// eGameState.Playing
+void function GameStateEnter_Playing()
+{
+ thread GameStateEnter_Playing_Threaded()
+}
+
+void function GameStateEnter_Playing_Threaded()
+{
+ WaitFrame() // ensure timelimits are all properly set
+
+ while ( GetGameState() == eGameState.Playing )
+ {
+ // could cache these, but what if we update it midgame?
+ float endTime
+ if ( IsRoundBased() )
+ endTime = expect float( GetServerVar( "roundEndTime" ) )
+ else
+ endTime = expect float( GetServerVar( "gameEndTime" ) )
+
+ // time's up!
+ if ( Time() >= endTime )
+ {
+ int winningTeam
+ if ( file.timeoutWinnerDecisionFunc != null )
+ winningTeam = file.timeoutWinnerDecisionFunc()
+ else
+ winningTeam = GameScore_GetWinningTeam()
+
+ if ( file.switchSidesBased && !file.hasSwitchedSides )
+ SetGameState( eGameState.SwitchingSides )
+ else if ( file.suddenDeathBased && winningTeam == TEAM_UNASSIGNED ) // suddendeath if we draw and suddendeath is enabled and haven't switched sides
+ SetGameState( eGameState.SuddenDeath )
+ else
+ SetWinner( winningTeam )
+ }
+
+ WaitFrame()
+ }
+}
+
+
+// eGameState.WinnerDetermined
+// these are likely innacurate
+const float ROUND_END_FADE_KILLREPLAY = 1.0
+const float ROUND_END_DELAY_KILLREPLAY = 3.0
+const float ROUND_END_FADE_NOKILLREPLAY = 8.0
+const float ROUND_END_DELAY_NOKILLREPLAY = 10.0
+
+void function GameStateEnter_WinnerDetermined()
+{
+ thread GameStateEnter_WinnerDetermined_Threaded()
+}
+
+void function GameStateEnter_WinnerDetermined_Threaded()
+{
+ WaitFrame() // wait a frame so other scripts can setup killreplay stuff
+
+ entity replayAttacker = file.roundWinningKillReplayAttacker
+ bool doReplay = Replay_IsEnabled() && Evac_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker )
+
+ float replayLength = 2.0 // extra delay if no replay
+ if ( doReplay )
+ {
+ replayLength = min( ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY, Time() - replayAttacker.s.respawnTime ) // 7.5s unless player lifetime < that
+ SetServerVar( "roundWinningKillReplayEntHealthFrac", file.roundWinningKillReplayHealthFrac )
+ }
+
+ foreach ( entity player in GetPlayerArray() )
+ thread PlayerWatchesRoundWinningKillReplay( player, doReplay, replayLength )
+
+ wait replayLength + ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
+
+ if ( IsRoundBased() )
+ {
+ svGlobal.levelEnt.Signal( "RoundEnd" )
+ SetServerVar( "roundsPlayed", GetServerVar( "roundsPlayed" ) + 1 )
+
+ if ( max( GameRules_GetTeamScore( TEAM_IMC ), GameRules_GetTeamScore( TEAM_MILITIA ) ) >= GameMode_GetRoundScoreLimit( GAMETYPE ) )
+ SetGameState( eGameState.Postmatch )
+ else if ( file.switchSidesBased && !file.hasSwitchedSides )
+ SetGameState( eGameState.SwitchingSides )
+ else if ( file.usePickLoadoutScreen )
+ SetGameState( eGameState.PickLoadout )
+ else
+ SetGameState ( eGameState.Prematch )
+ }
+ else
+ {
+ if ( Evac_IsEnabled() )
+ SetGameState( eGameState.Epilogue )
+ else
+ SetGameState( eGameState.Postmatch )
+ }
+}
+
+void function PlayerWatchesRoundWinningKillReplay( entity player, bool doReplay, float replayLength )
+{
+ player.FreezeControlsOnServer()
+
+ int winningTeam = GetWinningTeam()
+ int announcementSubstr
+ if ( winningTeam != TEAM_UNASSIGNED )
+ announcementSubstr = player.GetTeam() == winningTeam ? file.announceRoundWinnerWinningSubstr : file.announceRoundWinnerLosingSubstr
+
+ if ( IsRoundBased() )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_AnnounceRoundWinner", winningTeam, announcementSubstr, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME, GameRules_GetTeamScore2( TEAM_MILITIA ), GameRules_GetTeamScore2( TEAM_IMC ) )
+ else
+ Remote_CallFunction_NonReplay( player, "ServerCallback_AnnounceWinner", winningTeam, announcementSubstr, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
+
+ if ( IsRoundBased() || !Evac_IsEnabled() ) // if we're doing evac, then no fades or killreplay
+ {
+ ScreenFadeToBlackForever( player, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
+ wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
+
+ // do this after screen goes black so people can't see the titan dying
+ // don't use .die since that makes explosions and that
+ // todo: need a function specifically for cleaning up npcs and stuff on round end, this is imperfect
+ if ( IsAlive( player.GetPetTitan() ) )
+ player.GetPetTitan().Destroy()
+
+ if ( doReplay )
+ {
+ player.SetPredictionEnabled( false ) // prediction fucks with replays
+
+ entity attacker = file.roundWinningKillReplayAttacker
+ player.SetKillReplayDelay( Time() - replayLength, THIRD_PERSON_KILL_REPLAY_ALWAYS )
+ player.SetKillReplayInflictorEHandle( attacker.GetEncodedEHandle() )
+ player.SetKillReplayVictim( file.roundWinningKillReplayVictim )
+ player.SetViewIndex( attacker.GetIndexForEntity() )
+ player.SetIsReplayRoundWinning( true )
+
+ if ( replayLength >= ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY - 0.5 ) // only do fade if close to full length replay
+ {
+ // this doesn't work because fades don't work on players that are in a replay, unsure how official servers do this
+ wait replayLength - 2.0
+ ScreenFadeToBlackForever( player, 2.0 )
+
+ wait 2.0
+ }
+ else
+ wait replayLength
+ }
+ else
+ wait replayLength // this will just be extra delay if no replay
+
+ player.SetPredictionEnabled( true )
+ player.ClearReplayDelay()
+ player.ClearViewEntity()
+ player.UnfreezeControlsOnServer()
+ }
+}
+
+
+// eGameState.SwitchingSides
+void function GameStateEnter_SwitchingSides()
+{
+ thread GameStateEnter_SwitchingSides_Threaded()
+}
+
+void function GameStateEnter_SwitchingSides_Threaded()
+{
+ entity replayAttacker = file.roundWinningKillReplayAttacker
+ bool doReplay = Replay_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker ) && !IsRoundBased() // for roundbased modes, we've already done the replay
+
+ float replayLength = SWITCHING_SIDES_DELAY_REPLAY // extra delay if no replay
+ if ( doReplay )
+ {
+ replayLength = min( SWITCHING_SIDES_DELAY, Time() - replayAttacker.s.respawnTime ) // 6s unless player lifetime < that
+ SetServerVar( "roundWinningKillReplayEntHealthFrac", file.roundWinningKillReplayHealthFrac )
+ }
+
+ foreach ( entity player in GetPlayerArray() )
+ thread PlayerWatchesSwitchingSidesKillReplay( player, doReplay, replayLength )
+
+ wait SWITCHING_SIDES_DELAY_REPLAY + replayLength
+
+ file.hasSwitchedSides = true
+ SetServerVar( "switchedSides", 1 )
+ file.roundWinningKillReplayAttacker = null // reset this after replay
+
+ if ( file.usePickLoadoutScreen )
+ SetGameState( eGameState.PickLoadout )
+ else
+ SetGameState ( eGameState.Prematch )
+}
+
+void function PlayerWatchesSwitchingSidesKillReplay( entity player, bool doReplay, float replayLength )
+{
+ player.FreezeControlsOnServer()
+
+ ScreenFadeToBlackForever( player, SWITCHING_SIDES_DELAY_REPLAY ) // automatically cleared
+ wait SWITCHING_SIDES_DELAY_REPLAY
+
+ // do this after screen goes black so people can't see the titan dying
+ // don't use .die since that makes explosions and that
+ if ( IsAlive( player.GetPetTitan() ) )
+ player.GetPetTitan().Destroy()
+
+ if ( doReplay )
+ {
+ player.SetPredictionEnabled( false ) // prediction fucks with replays
+
+ entity attacker = file.roundWinningKillReplayAttacker
+ player.SetKillReplayDelay( Time() - replayLength, THIRD_PERSON_KILL_REPLAY_ALWAYS )
+ player.SetKillReplayInflictorEHandle( attacker.GetEncodedEHandle() )
+ player.SetKillReplayVictim( file.roundWinningKillReplayVictim )
+ player.SetViewIndex( attacker.GetIndexForEntity() )
+ player.SetIsReplayRoundWinning( true )
+
+ if ( replayLength >= SWITCHING_SIDES_DELAY - 0.5 ) // only do fade if close to full length replay
+ {
+ // this doesn't work because fades don't work on players that are in a replay, unsure how official servers do this
+ wait replayLength - ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
+ ScreenFadeToBlackForever( player, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME )
+
+ wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME
+ }
+ else
+ wait replayLength
+ }
+ else
+ wait SWITCHING_SIDES_DELAY_REPLAY // extra delay if no replay
+
+ player.SetPredictionEnabled( true )
+ player.ClearReplayDelay()
+ player.ClearViewEntity()
+ player.UnfreezeControlsOnServer()
+}
+
+
+// eGameState.SuddenDeath
+void function GameStateEnter_SuddenDeath()
+{
+ // disable respawns, suddendeath calling is done on a kill callback
+ SetRespawnsEnabled( false )
+}
+
+void function GameStateEnter_SuddenDeath_Threaded()
+{
+ while ( GetGameState() == eGameState.SuddenDeath )
+ {
+ // todo this really ought to work for ffa in the future
+ int imcPlayers
+ int militiaPlayers
+
+ foreach ( entity player in GetPlayerArray() )
+ {
+ if ( IsAlive( player ) )
+ {
+ if ( player.GetTeam() == TEAM_IMC )
+ imcPlayers++
+ else
+ militiaPlayers++
+ }
+ }
+
+ if ( imcPlayers == 0 )
+ SetWinner( TEAM_MILITIA )
+ else if ( militiaPlayers == 0 )
+ SetWinner( TEAM_IMC )
+
+ WaitFrame()
+ }
+}
+
+
+// eGameState.Postmatch
+void function GameStateEnter_Postmatch()
+{
+ foreach ( entity player in GetPlayerArray() )
+ {
+ player.FreezeControlsOnServer()
+ thread ForceFadeToBlack( player )
+ }
+
+ thread GameStateEnter_Postmatch_Threaded()
+}
+
+void function GameStateEnter_Postmatch_Threaded()
+{
+ wait GAME_POSTMATCH_LENGTH
+
+ GameRules_EndMatch()
+}
+
+void function ForceFadeToBlack( entity player )
+{
+ // hack until i figure out what deathcam stuff is causing fadetoblacks to be cleared
+ while ( true )
+ {
+ WaitFrame()
+ ScreenFadeToBlackForever( player, 0.0 )
+ }
+}
+
+
+// shared across multiple gamestates
+void function SetSkyCam( entity player )
+{
+ entity skycam = GetEnt( "skybox_cam_level" )
+
+ if ( skycam != null )
+ player.SetSkyCamera( skycam )
+}
+
+void function OnPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ if ( !GamePlayingOrSuddenDeath() )
+ return
+
+ // set round winning killreplay info here if no custom replaydelay
+ if ( file.roundWinningKillReplayTrackPilotKills )
+ {
+ file.roundWinningKillReplayVictim = victim
+ file.roundWinningKillReplayAttacker = attacker
+ file.roundWinningKillReplayMethodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ file.roundWinningKillReplayTimeOfDeath = Time()
+ file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
+ }
+
+ // note: pilotstitans is just win if enemy team runs out of either pilots or titans
+ if ( IsPilotEliminationBased() || GetGameState() == eGameState.SuddenDeath )
+ {
+ if ( GetPlayerArrayOfTeam_Alive( victim.GetTeam() ).len() == 0 )
+ {
+ // for ffa we need to manually get the last team alive
+ if ( IsFFAGame() )
+ {
+ array<int> teamsWithLivingPlayers
+ foreach ( entity player in GetPlayerArray_Alive() )
+ {
+ if ( !teamsWithLivingPlayers.contains( player.GetTeam() ) )
+ teamsWithLivingPlayers.append( player.GetTeam() )
+ }
+
+ if ( teamsWithLivingPlayers.len() == 1 )
+ SetWinner( teamsWithLivingPlayers[ 0 ], "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" )
+ else if ( teamsWithLivingPlayers.len() == 0 ) // failsafe: only team was the dead one
+ SetWinner( TEAM_UNASSIGNED, "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" ) // this is fine in ffa
+ }
+ else
+ SetWinner( GetOtherTeam( victim.GetTeam() ), "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" )
+ }
+ }
+
+ if ( ( Riff_EliminationMode() == eEliminationMode.Titans || Riff_EliminationMode() == eEliminationMode.PilotsTitans ) && victim.IsTitan() ) // need an extra check for this
+ OnTitanKilled( victim, damageInfo )
+}
+
+void function OnTitanKilled( entity victim, var damageInfo )
+{
+ if ( !GamePlayingOrSuddenDeath() )
+ return
+
+ // set round winning killreplay info here if no custom replaydelay
+ if ( file.roundWinningKillReplayTrackTitanKills )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ file.roundWinningKillReplayVictim = victim
+ file.roundWinningKillReplayAttacker = attacker
+ file.roundWinningKillReplayMethodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ file.roundWinningKillReplayTimeOfDeath = Time()
+ file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
+ }
+
+ // note: pilotstitans is just win if enemy team runs out of either pilots or titans
+ if ( IsTitanEliminationBased() )
+ {
+ int livingTitans
+ foreach ( entity titan in GetTitanArrayOfTeam( victim.GetTeam() ) )
+ livingTitans++
+
+ if ( livingTitans == 0 )
+ {
+ // for ffa we need to manually get the last team alive
+ if ( IsFFAGame() )
+ {
+ array<int> teamsWithLivingTitans
+ foreach ( entity titan in GetTitanArray() )
+ {
+ if ( !teamsWithLivingTitans.contains( titan.GetTeam() ) )
+ teamsWithLivingTitans.append( titan.GetTeam() )
+ }
+
+ if ( teamsWithLivingTitans.len() == 1 )
+ SetWinner( teamsWithLivingTitans[ 0 ], "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" )
+ else if ( teamsWithLivingTitans.len() == 0 ) // failsafe: only team was the dead one
+ SetWinner( TEAM_UNASSIGNED, "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" ) // this is fine in ffa
+ }
+ else
+ SetWinner( GetOtherTeam( victim.GetTeam() ), "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" )
+ }
+ }
+}
+
+
+
+// stuff for gamemodes to call
+
+void function SetShouldUsePickLoadoutScreen( bool shouldUse )
+{
+ file.usePickLoadoutScreen = shouldUse
+}
+
+void function SetSwitchSidesBased( bool switchSides )
+{
+ file.switchSidesBased = switchSides
+}
+
+void function SetSuddenDeathBased( bool suddenDeathBased )
+{
+ file.suddenDeathBased = suddenDeathBased
+}
+
+void function SetShouldUseRoundWinningKillReplay( bool shouldUse )
+{
+ SetServerVar( "roundWinningKillReplayEnabled", shouldUse )
+}
+
+// is this necessary? idk really
+void function SetRoundWinningKillReplayKillClasses( bool pilot, bool titan )
+{
+ file.roundWinningKillReplayTrackPilotKills = pilot
+ file.roundWinningKillReplayTrackTitanKills = titan // player kills in titans should get tracked anyway, might be worth renaming this
+}
+
+void function SetRoundWinningKillReplayAttacker( entity target )
+{
+ file.roundWinningKillReplayAttacker = target
+}
+
+void function SetWinner( int team, string winningReason = "", string losingReason = "" )
+{
+ SetServerVar( "winningTeam", team )
+
+ if ( winningReason.len() == 0 )
+ file.announceRoundWinnerWinningSubstr = 0
+ else
+ file.announceRoundWinnerWinningSubstr = GetStringID( winningReason )
+
+ if ( losingReason.len() == 0 )
+ file.announceRoundWinnerLosingSubstr = 0
+ else
+ file.announceRoundWinnerLosingSubstr = GetStringID( losingReason )
+
+ if ( IsRoundBased() )
+ {
+ if ( team != TEAM_UNASSIGNED )
+ {
+ GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + 1 )
+ GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + 1 )
+ }
+
+ SetGameState( eGameState.WinnerDetermined )
+ }
+ else
+ SetGameState( eGameState.WinnerDetermined )
+}
+
+void function AddTeamScore( int team, int amount )
+{
+ GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + amount )
+ GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + amount )
+
+ int scoreLimit
+ if ( IsRoundBased() )
+ scoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE )
+ else
+ scoreLimit = GameMode_GetScoreLimit( GAMETYPE )
+
+ int score = GameRules_GetTeamScore( team )
+ if ( score >= scoreLimit || GetGameState() == eGameState.SuddenDeath )
+ SetWinner( team )
+ else if ( ( file.switchSidesBased && !file.hasSwitchedSides ) && score >= ( scoreLimit.tofloat() / 2.0 ) )
+ SetGameState( eGameState.SwitchingSides )
+}
+
+void function SetRoundWinningKillReplayInfo( entity victim, entity attacker, int methodOfDeath, float timeOfDeath ) // can't just pass in a damageinfo because they seem to die over time somehow
+{
+ file.roundWinningKillReplayVictim = victim
+ file.roundWinningKillReplayAttacker = attacker
+ file.roundWinningKillReplayMethodOfDeath = methodOfDeath
+ file.roundWinningKillReplayTimeOfDeath = timeOfDeath
+ if ( attacker != null )
+ file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker )
+}
+
+void function SetTimeoutWinnerDecisionFunc( int functionref() callback )
+{
+ file.timeoutWinnerDecisionFunc = callback
+}
+
+// idk
+
+float function GameState_GetTimeLimitOverride()
+{
+ return 100
+}
+
+bool function IsRoundBasedGameOver()
+{
+ return false
+}
+
+bool function ShouldRunEvac()
+{
+ return true
+}
+
+void function GiveTitanToPlayer(entity player)
+{
+
+}
+
+float function GetTimeLimit_ForGameMode()
+{
+ return 100.0
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_goblin_dropship.nut b/Northstar.CustomServers/scripts/vscripts/mp/_goblin_dropship.nut
new file mode 100644
index 000000000..fe36e6681
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_goblin_dropship.nut
@@ -0,0 +1,784 @@
+untyped
+
+global function GoblinDropship_Init
+
+#if MP
+ global function GetZiplineDropshipSpawns
+#endif //MP
+global function RunDropshipDropoff
+global function DropshipFindDropNodes
+global function AnaylsisFuncDropshipFindDropNodes
+global function AddTurret
+global function SetDropTableSpawnFuncs
+
+const LINEGEN_DEBUG = 0
+global const bool FLIGHT_PATH_DEBUG = false
+const LINEGEN_TIME = 600.0
+
+const OPTIMAL_ZIPNODE_DIST_SQRD = 16384 //128 sqrd
+// 4096 64 sqrd
+// 65536 256 sqrd
+
+struct
+{
+ array<entity> ziplineDropshipSpawns
+
+ table < var, var > dropshipSound = {
+ [ TEAM_IMC ] = {
+ [ DROPSHIP_STRAFE ] = "Goblin_IMC_TroopDeploy_Flyin",
+ [ DROPSHIP_VERTICAL ] = "Goblin_Dropship_Flyer_Attack_Vertical_Succesful",
+ [ DROPSHIP_FLYER_ATTACK_ANIM_VERTICAL ] = "Goblin_Flyer_Dropshipattack_Vertical",
+ [ DROPSHIP_FLYER_ATTACK_ANIM ] = "Goblin_Flyer_Dropshipattack"
+ },
+ [ TEAM_MILITIA ] = {
+ [ DROPSHIP_STRAFE ] = "Crow_MCOR_TroopDeploy_Flyin",
+ [ DROPSHIP_VERTICAL ] = "Crow_Dropship_Flyer_Attack_Vertical_Succesful",
+ [ DROPSHIP_FLYER_ATTACK_ANIM_VERTICAL ] = "Crow_Flyer_Dropshipattack_Vertical",
+ [ DROPSHIP_FLYER_ATTACK_ANIM ] = "Crow_Flyer_Dropshipattack"
+ }
+ }
+
+} file
+
+function GoblinDropship_Init()
+{
+ RegisterSignal( "OnDropoff" )
+ RegisterSignal( "embark" )
+ RegisterSignal( "WarpedIn" )
+ PrecacheImpactEffectTable( "dropship_dust" )
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+}
+
+void function EntitiesDidLoad()
+{
+ //Generate a list of valid zipline dropship drop off points.
+ #if MP
+ BuildZiplineDropshipSpawnPoints()
+ #endif //MP
+}
+
+#if MP
+void function BuildZiplineDropshipSpawnPoints()
+{
+ array<entity> spawnPoints = SpawnPoints_GetDropPod()
+ file.ziplineDropshipSpawns = []
+
+ foreach ( entity spawnPoint in spawnPoints )
+ {
+ if ( !DropshipCanZiplineDropAtSpawnPoint( spawnPoint ) )
+ continue
+
+ file.ziplineDropshipSpawns.append( spawnPoint )
+ }
+
+ //Assert( file.dropshipSpawns.len() > 0, "No valid zipline dropship spawns exist in this map." )
+}
+
+//Function returns an array of level droppod spawns that have been pretested to ensure they have the space for zipline deployments.
+array<entity> function GetZiplineDropshipSpawns()
+{
+ return clone file.ziplineDropshipSpawns
+}
+#endif //MP
+
+bool function AnaylsisFuncDropshipFindDropNodes( FlightPath flightPath, vector origin, float yaw )
+{
+ return DropshipFindDropNodes( flightPath, origin, yaw, "both", false, IsLegalFlightPath ).len() != 0
+}
+
+// run from TryAnalysisAtOrigin
+table<string,table<string,NodeFP> > function DropshipFindDropNodes( FlightPath flightPath, vector origin, float yaw, string side = "both", ignoreCollision = false, bool functionref( FlightPath, vector, vector, vector, bool = 0 ) legalFlightFunc = null, bool amortize = false )
+{
+ // find nodes to deploy to
+ table<string,table<string,NodeFP> > foundNodes
+
+ vector angles = Vector( 0, yaw, 0 )
+ vector forward = AnglesToForward( angles )
+ vector right = AnglesToRight( angles )
+ Point start = GetWarpinPosition( flightPath.model, flightPath.anim, origin, angles )
+ if ( fabs( start.origin.x ) > MAX_WORLD_COORD )
+ return {}
+ if ( fabs( start.origin.y ) > MAX_WORLD_COORD )
+ return {}
+ if ( fabs( start.origin.z ) > MAX_WORLD_COORD )
+ return {}
+
+ if ( !ignoreCollision )
+ {
+ if ( !legalFlightFunc( flightPath, origin, forward, right, FLIGHT_PATH_DEBUG && !IsActiveNodeAnalysis() ) )
+ return {}
+ }
+
+ Point deployPoint = GetPreviewPoint( flightPath )
+ vector deployOrigin = GetOriginFromPoint( deployPoint, origin, forward, right )
+ vector deployAngles = GetAnglesFromPoint( deployPoint, angles )
+ float deployYaw = deployAngles.y
+
+ // flatten it
+ deployAngles.x = 0
+ deployAngles.z = 0
+
+ float pitch = 50
+ vector deployRightAngles = AnglesCompose( deployAngles, Vector( 0, -90, 0 ) )
+ deployRightAngles = AnglesCompose( deployRightAngles, Vector( pitch, 0, 0 ) )
+
+ vector deployLeftAngles = AnglesCompose( deployAngles, Vector( 0, 90, 0 ) )
+ deployLeftAngles = AnglesCompose( deployLeftAngles, Vector( pitch, 0, 0 ) )
+
+ table<int,NodeFP> nodeTable
+ bool foundRightNodes = false
+ bool foundLeftNodes = false
+
+ if ( side == "right" || side == "both" || side == "either" )
+ {
+ nodeTable = FindDropshipDeployNodes( deployOrigin, deployRightAngles, amortize )
+ if ( LINEGEN_DEBUG )
+ {
+ foreach( node in nodeTable )
+ DebugDrawLine( deployOrigin, node.origin, 200, 200, 200, true, 30.0 )
+ }
+
+ if ( nodeTable.len() )
+ {
+ if ( amortize )
+ WaitFrame()
+ foundRightNodes = FindBestDropshipNodesForSide( foundNodes, nodeTable, "right", flightPath, origin, forward, right, angles, deployOrigin, deployRightAngles, amortize )
+ }
+
+ if ( !foundRightNodes && side != "either" )
+ return {}
+
+ if ( amortize )
+ WaitFrame()
+ }
+
+ if ( side == "left" || side == "both" || side == "either" )
+ {
+ nodeTable = FindDropshipDeployNodes( deployOrigin, deployLeftAngles, amortize )
+ if ( nodeTable.len() )
+ {
+ if ( amortize )
+ WaitFrame()
+ foundLeftNodes = FindBestDropshipNodesForSide( foundNodes, nodeTable, "left", flightPath, origin, forward, right, angles, deployOrigin, deployLeftAngles, amortize )
+ }
+
+ if ( !foundLeftNodes && side != "either" )
+ return {}
+ }
+
+ if ( !foundRightNodes && !foundLeftNodes )
+ return {}
+
+ if ( LINEGEN_DEBUG || FLIGHT_PATH_DEBUG )
+ {
+ DrawArrow( origin, angles, 15.0, 250 )
+ float time = 500.0
+ foreach ( side, nodes in foundNodes )
+ {
+ //DebugDrawText( nodes.centerNode.origin + Vector(0,0,55), nodes.centerNode.fraction + "", true, time )
+ //DebugDrawText( nodes.centerNode.origin, "" + nodes.centerNode.dot, true, time )
+ DebugDrawLine( nodes.centerNode.origin, nodes.centerNode.attachOrigin, 120, 255, 120, true, time )
+ DebugDrawCircle( nodes.centerNode.origin, Vector( 0,0,0 ), 15, 120, 255, 120, true, time )
+
+ //DebugDrawText( nodes.leftNode.origin + Vector(0,0,55), nodes.leftNode.fraction + "", true, time )
+ //DebugDrawText( nodes.leftNode.origin, "" + nodes.leftNode.dot, true, time )
+ DebugDrawLine( nodes.leftNode.origin, nodes.leftNode.attachOrigin, 255, 120, 120, true, time )
+ DebugDrawCircle( nodes.leftNode.origin, Vector( 0,0,0 ), 15, 255, 120, 120, true, time )
+
+ //DebugDrawText( nodes.rightNode.origin + Vector(0,0,55), nodes.rightNode.fraction + "", true, time )
+ //DebugDrawText( nodes.rightNode.origin, "" + nodes.rightNode.dot, true, time )
+ DebugDrawLine( nodes.rightNode.origin, nodes.rightNode.attachOrigin, 120, 120, 255, true, time )
+ DebugDrawCircle( nodes.rightNode.origin, Vector( 0,0,0 ), 15, 120, 120, 255, true, time )
+
+ //DebugDrawLine( nodes.rightNode.origin, nodes.centerNode.origin, 200, 200, 200, true, time )
+ //DebugDrawText( nodes.rightNode.origin + Vector(0,0,20), "dist: " + Distance( nodes.rightNode.origin, nodes.centerNode.origin ), true, time )
+ //DebugDrawLine( nodes.leftNode.origin, nodes.centerNode.origin, 200, 200, 200, true, time )
+ //DebugDrawText( nodes.leftNode.origin + Vector(0,0,20), "dist: " + Distance( nodes.leftNode.origin, nodes.centerNode.origin ), true, time )
+
+ //DebugDrawLine( origin, origin + deployForward * 200, 50, 255, 50, true, time )
+
+ // foreach ( node in nodes.rightNodes )
+ // {
+ // DebugDrawText( node.origin + Vector(0,0,25), "R", true, 15 )
+ // }
+ //
+ // foreach ( node in nodes.leftNodes )
+ // {
+ // DebugDrawText( node.origin + Vector(0,0,25), "L", true, 15 )
+ // }
+ }
+
+// IsLegalFlightPath( flightPath, origin, forward, right, true )
+ }
+
+ return foundNodes
+}
+
+
+table<int,NodeFP> function FindDropshipDeployNodes( vector deployOrigin, vector deployAngles, bool amortize = false )
+{
+ vector deployForward = AnglesToForward( deployAngles )
+
+ vector end = deployOrigin + deployForward * 3000
+ TraceResults result = TraceLine( deployOrigin, end, null, TRACE_MASK_NPCWORLDSTATIC )
+
+ if ( LINEGEN_DEBUG )
+ {
+ DebugDrawLine( deployOrigin, result.endPos, 255, 255, 255, true, LINEGEN_TIME )
+ DebugDrawText( result.endPos + Vector( 0,0,10 ), "test", true, LINEGEN_TIME )
+ DebugDrawCircle( result.endPos, Vector( 0,0,0 ), 35, 255, 255, 255, true, LINEGEN_TIME )
+ }
+ // no hit?
+ if ( result.fraction >= 1.0 )
+ return {}
+
+ int node = GetNearestNodeToPos( result.endPos )
+ if ( node == -1 )
+ return {}
+
+ if ( LINEGEN_DEBUG )
+ {
+ DebugDrawText( GetNodePos( node ) + Vector(0,0,10), "nearest node", true, 15.0 )
+ DebugDrawCircle( GetNodePos( node ), Vector( 0,0,0 ), 20, 60, 60, 255, true, LINEGEN_TIME )
+ }
+
+ array<vector> neighborPositions = NavMesh_GetNeighborPositions( GetNodePos( node ), HULL_HUMAN, 20 )
+
+ if ( amortize )
+ WaitFrame()
+
+ table<int,NodeFP> nodeTable = {}
+ int uniqueID = -2
+ foreach ( pos in neighborPositions )
+ {
+ NodeFP attachPoint
+ attachPoint.origin = pos
+ attachPoint.uniqueID = uniqueID
+
+ nodeTable[ uniqueID ] <- attachPoint
+ uniqueID--
+ }
+
+ return nodeTable
+}
+
+void function AddDirectionVec( array<NodeFP> nodeArray, vector origin )
+{
+ // different direction vecs because we want a node to the left, center, and straight
+ foreach ( node, tab in nodeArray )
+ {
+ vector vec
+ vec = tab.origin - origin
+ vec.Norm()
+ tab.vec = vec
+ }
+}
+
+void function AddDirectionVecFromDir( array<NodeFP> nodeArray, vector origin, vector dir )
+{
+ // different direction vecs because we want a node to the left, center, and straight
+ foreach ( node, tab in nodeArray )
+ {
+ vector vec
+ vec = ( tab.origin + dir * 50 ) - origin
+ vec.Norm()
+ tab.vec = vec
+ }
+}
+
+bool function FindBestDropshipNodesForSide( table<string,table<string,NodeFP> > foundNodes, table<int,NodeFP> nodeTable, string side, FlightPath flightPath, vector origin, vector forward, vector right, vector angles, vector deployOrigin, vector deployAngles, bool amortize )
+{
+ vector deployForward = AnglesToForward( deployAngles )
+ vector deployRight = AnglesToRight( deployAngles )
+
+ float RatioForLeftRight = 0.2
+ vector RightDeployForward = ( ( deployForward * ( 1.0 - RatioForLeftRight ) ) + ( deployRight * RatioForLeftRight * -1 ) )
+ RightDeployForward.Norm()
+ vector LeftDeployForward = ( ( deployForward * ( 1.0 - RatioForLeftRight ) ) + ( deployRight * RatioForLeftRight ) )
+ LeftDeployForward.Norm()
+
+ if ( amortize )
+ WaitFrame()
+
+ foundNodes[ side ] <- {}
+ array<AttachPoint> attachPoints = GetAttachPoints( flightPath, side )
+
+ array<NodeFP> centerNodes = GetNodeArrayFromTable( nodeTable )
+ AddDirectionVec( centerNodes, deployOrigin )
+ NodeFP centerNode = GetBestDropshipNode( attachPoints[2], centerNodes, origin, deployForward, forward, right, angles, NullNodeFP )
+ if ( centerNode == NullNodeFP )
+ return false
+ delete nodeTable[ centerNode.uniqueID ]
+
+ if ( amortize )
+ WaitFrame()
+
+ array<NodeFP> leftNodes = GetCulledNodes( nodeTable, deployRight * -1 )
+ AddDirectionVecFromDir( leftNodes, deployOrigin, deployRight * -1 )
+ NodeFP leftNode = GetBestDropshipNode( attachPoints[1], leftNodes, origin, RightDeployForward, forward, right, angles, centerNode )
+ if ( leftNode == NullNodeFP )
+ return false
+ delete nodeTable[ leftNode.uniqueID ]
+
+ if ( amortize )
+ WaitFrame()
+
+ array<NodeFP> rightNodes = GetCulledNodes( nodeTable, deployRight )
+ AddDirectionVecFromDir( rightNodes, deployOrigin, deployRight )
+ NodeFP rightNode = GetBestDropshipNode( attachPoints[0], rightNodes, origin, LeftDeployForward, forward, right, angles, centerNode )
+ if ( rightNode == NullNodeFP )
+ return false
+
+ table<string,NodeFP> Table
+ Table.centerNode <- centerNode
+ Table.leftNode <- leftNode
+ Table.rightNode <- rightNode
+
+ //Table.rightNodes <- rightNodes // for debug
+ //Table.leftNodes <- leftNodes // for debug
+
+ foundNodes[ side ] = Table
+ return true
+}
+
+array<NodeFP> function GetNodeArrayFromTable( table<int,NodeFP> nodeTable )
+{
+ array<NodeFP> Array
+ foreach ( Table in nodeTable )
+ {
+ Array.append( Table )
+ }
+
+ return Array
+}
+
+array<NodeFP> function GetCulledNodes( table<int,NodeFP> nodeTable, vector right )
+{
+ table<int,NodeFP> leftNodes
+ // get the nodes on the left
+ foreach ( nod, tab in nodeTable )
+ {
+ float dot = DotProduct( tab.vec, right )
+ if ( dot >= 0.0 )
+ {
+ leftNodes[ nod ] <- tab
+ }
+ }
+
+ return GetNodeArrayFromTable( leftNodes )
+}
+
+NodeFP function GetBestDropshipNode( AttachPoint attachPoint, array<NodeFP> nodeArray, vector origin, vector deployForward, vector forward, vector right, vector angles, NodeFP centerNode, bool showdebug = false )
+{
+ foreach ( node in nodeArray )
+ {
+ node.dot = DotProduct( node.vec, deployForward )
+ if ( showdebug )
+ {
+ DebugDrawText( node.origin, "dot: " + node.dot, true, 15.0 )
+ int green = 0
+ int red = 255
+ if ( node.dot > 0.9 )
+ {
+ float frac = ( 1.0 - node.dot ) / 0.1
+ frac = 1.0 - frac
+
+ green = int( frac * 255 )
+ red -= green
+ }
+
+ DebugDrawLine( node.origin, node.origin + ( node.vec * -1000 ), red, green, 0, true, 15.0 )
+ DebugDrawCircle( node.origin, Vector( 0,0,0 ), 25, red, green, 0, true, 15.0 )
+ }
+ }
+
+ if ( !nodeArray.len() )
+ return NullNodeFP
+
+ vector attachOrigin = GetOriginFromAttachPoint( attachPoint, origin, forward, right )
+ vector attachAngles = GetAnglesFromAttachPoint( attachPoint, angles )
+ vector attachForward = AnglesToForward( attachAngles )
+ vector attachRight = AnglesToRight( attachAngles )
+
+ FlightPath offsetAnalysis = GetAnalysisForModel( TEAM_IMC_GRUNT_MODEL, ZIPLINE_IDLE_ANIM )
+ Point offsetPoint = GetPreviewPoint( offsetAnalysis )
+ vector offsetOrigin = GetOriginFromPoint( offsetPoint, attachOrigin, attachForward, attachRight )
+// DebugDrawLine( offsetOrigin, attachOrigin, 255, 255, 0, true, 15 )
+
+ nodeArray.sort( SortHighestDot )
+
+ vector mins = GetBoundsMin( HULL_HUMAN )
+ vector maxs = GetBoundsMax( HULL_HUMAN )
+
+ array<NodeFP> passedNodes
+
+ for ( int i = 0; i < nodeArray.len(); i++ )
+ {
+ NodeFP node = nodeArray[i]
+
+ // beyond the allowed dot
+ if ( node.dot < 0.3 )
+ return NullNodeFP
+
+ // trace to see if the ai could drop to the node from here
+ TraceResults result = TraceHull( offsetOrigin, node.origin, mins, maxs, null, TRACE_MASK_NPCWORLDSTATIC, TRACE_COLLISION_GROUP_NONE )
+ if ( result.fraction < 1.0 )
+ continue //return
+
+ // trace to insure that there will be a good place to hook the zipline
+ if ( !GetHookOriginFromNode( offsetOrigin, node.origin, attachOrigin ) )
+ continue
+
+ node.fraction = result.fraction
+ node.attachOrigin = offsetOrigin
+ node.attachName = attachPoint.name
+
+ if ( centerNode != NullNodeFP )
+ {
+ //test for distance, not too close, not too far
+ local distSqr = DistanceSqr( centerNode.origin, node.origin )
+ node.rating = fabs( OPTIMAL_ZIPNODE_DIST_SQRD - distSqr )
+ passedNodes.append( node )
+ continue
+ }
+
+ return node
+ }
+
+ if ( centerNode != NullNodeFP && passedNodes.len() )
+ {
+ passedNodes.sort( SortLowestRating )
+ return passedNodes[ 0 ]
+ }
+
+ return NullNodeFP
+}
+
+int function SortHighestDot( NodeFP a, NodeFP b )
+{
+ if ( a.dot > b.dot )
+ return -1
+
+ if ( a.dot < b.dot )
+ return 1
+
+ return 0
+}
+
+int function SortLowestRating( NodeFP a, NodeFP b )
+{
+ if ( a.rating > b.rating )
+ return 1
+
+ if ( a.rating < b.rating )
+ return -1
+
+ return 0
+}
+
+void function SetDropTableSpawnFuncs( CallinData drop, entity functionref( int, vector, vector ) spawnFunc, int count )
+{
+ array<entity functionref( int, vector, vector )> spawnFuncArray
+ //spawnFuncArray.resize( count, spawnFunc )
+ for ( int i = 0; i < count; i++ )
+ {
+ spawnFuncArray.append( spawnFunc )
+ }
+ drop.npcSpawnFuncs = spawnFuncArray
+}
+
+asset function GetTeamDropshipModel( int team, bool hero = false )
+{
+ if ( hero )
+ {
+ if ( team == TEAM_IMC )
+ return GetFlightPathModel( "fp_dropship_hero_model" )
+ else
+ return GetFlightPathModel( "fp_crow_hero_model" )
+ }
+ else
+ {
+ if ( team == TEAM_IMC )
+ return GetFlightPathModel( "fp_dropship_model" )
+ else
+ return GetFlightPathModel( "fp_crow_model" )
+ }
+
+ unreachable
+}
+
+//This function tests to see if the given spawn point has enough clearance for a dropship to deploy zipline grunts.
+bool function DropshipCanZiplineDropAtSpawnPoint( entity spawnPoint )
+{
+ CallinData drop
+ drop.origin = spawnPoint.GetOrigin()
+ drop.yaw = spawnPoint.GetAngles().y
+ drop.dist = 768
+ SetCallinStyle( drop, eDropStyle.ZIPLINE_NPC )
+ int style = drop.style
+
+ bool validSpawn = false
+ array<string> anims = GetRandomDropshipDropoffAnims()
+
+ string animation
+ FlightPath flightPath
+
+ foreach ( anim in anims )
+ {
+ animation = anim
+ flightPath = GetAnalysisForModel( DROPSHIP_MODEL, anim )
+
+ if ( style == eDropStyle.NONE )
+ {
+ if ( !drop.yawSet )
+ {
+ style = eDropStyle.NEAREST
+ }
+ else
+ {
+ style = eDropStyle.NEAREST_YAW
+ }
+ }
+
+ validSpawn = TestSpawnPointForStyle( flightPath, drop )
+
+ if ( validSpawn )
+ return true
+ }
+
+ return false
+}
+
+function RunDropshipDropoff( CallinData Table )
+{
+ vector origin = Table.origin
+ float yaw = Table.yaw
+ int team = Table.team
+ entity owner = Table.owner
+ string squadname = Table.squadname
+ string side = Table.side
+ array<entity functionref( int, vector, vector )> npcSpawnFuncs = Table.npcSpawnFuncs
+ int style = Table.style
+ int health = 7800
+
+ if ( Table.dropshipHealth != 0 )
+ health = Table.dropshipHealth
+ Table.success = false
+
+ if ( Flag( "DisableDropships" ) )
+ return
+
+ if ( team == 0 )
+ {
+ if ( owner )
+ team = owner.GetTeam()
+ else
+ team = 0
+ }
+
+ SpawnPointFP spawnPoint
+ array<string> anims = GetRandomDropshipDropoffAnims()
+
+ // Override anim, level scripter takes responsibility for it working in this location or not
+ if ( Table.anim != "" )
+ {
+ anims.clear()
+ anims.append( Table.anim )
+ }
+
+ string animation
+ FlightPath flightPath
+ bool wasPlayerOwned = IsValid( owner ) && IsValidPlayer( owner )
+
+ foreach ( anim in anims )
+ {
+ animation = anim
+ flightPath = GetAnalysisForModel( DROPSHIP_MODEL, anim )
+
+ if ( style == eDropStyle.NONE )
+ {
+ if ( !Table.yawSet )
+ {
+ style = eDropStyle.NEAREST
+ }
+ else
+ {
+ style = eDropStyle.NEAREST_YAW
+ }
+ }
+
+ spawnPoint = GetSpawnPointForStyle( flightPath, Table )
+
+ if ( spawnPoint.valid )
+ break
+ }
+
+ if ( !spawnPoint.valid )
+ {
+ printt( "Couldn't find good spawn location for dropship" )
+ return
+ }
+
+ Table.success = true
+
+ entity ref = CreateScriptRef()
+ if ( Table.forcedPosition )
+ {
+ ref.SetOrigin( Table.origin )
+ ref.SetAngles( Vector( 0, Table.yaw, 0 ) )
+ }
+ else
+ {
+ ref.SetOrigin( spawnPoint.origin )
+ ref.SetAngles( spawnPoint.angles )
+ }
+
+ // used for when flyers attack dropships
+ if ( "nextDropshipAttackedByFlyers" in level && level.nextDropshipAttackedByFlyers )
+ animation = FlyersAttackDropship( ref, animation )
+
+ Assert( IsNewThread(), "Must be threaded off" )
+
+ DropTable dropTable
+
+ if ( Table.dropTable.valid )
+ {
+ dropTable = Table.dropTable
+ }
+ else
+ {
+ bool ignoreCollision = true // = style == eDropStyle.FORCED
+ thread FindDropshipZiplineNodes( dropTable, flightPath, ref.GetOrigin(), ref.GetAngles(), side, ignoreCollision, true )
+ }
+
+ asset model = GetTeamDropshipModel( team )
+ waitthread WarpinEffect( model, animation, ref.GetOrigin(), ref.GetAngles() )
+ entity dropship = CreateDropship( team, ref.GetOrigin(), ref.GetAngles() )
+ SetSpawnOption_SquadName( dropship, squadname )
+ dropship.kv.solid = SOLID_VPHYSICS
+ DispatchSpawn( dropship )
+ Table.dropship = dropship
+ //dropship.SetPusher( true )
+ dropship.SetHealth( health )
+ dropship.SetMaxHealth( health )
+ Table.dropship = dropship
+ dropship.EndSignal( "OnDeath" )
+ dropship.Signal( "WarpedIn" )
+ ref.Signal( "WarpedIn" )
+ Signal( Table, "WarpedIn" )
+
+ AddDropshipDropTable( dropship, dropTable ) // this is where the ai will drop to
+
+ if ( IsValid( owner ) )
+ {
+ dropship.SetCanCloak( false )
+ dropship.SetOwner( owner )
+ if ( owner.IsPlayer() )
+ dropship.SetBossPlayer( owner )
+ }
+
+ local dropshipSound = GetTeamDropshipSound( team, animation )
+ if ( Table.customSnd != "" )
+ dropshipSound = Table.customSnd
+
+ OnThreadEnd(
+ function() : ( dropship, ref, Table, dropshipSound )
+ {
+ ref.Destroy()
+ if ( IsValid( dropship ) )
+ StopSoundOnEntity( dropship, dropshipSound )
+ if ( IsAlive( dropship ) )
+ {
+ dropship.Destroy()
+ }
+
+ Signal( Table, "OnDropoff", { guys = null } )
+ }
+ )
+
+ array<entity> guys
+ if ( !wasPlayerOwned || IsValidPlayer( owner ) )
+ {
+ guys = CreateNPCSForDropship( dropship, Table.npcSpawnFuncs, side )
+
+ foreach ( guy in guys )
+ {
+ if ( IsAlive( guy ) )
+ {
+ if ( IsValidPlayer( owner ) )
+ {
+ NPCFollowsPlayer( guy, owner )
+ }
+ }
+ }
+ }
+
+ //thread DropshipMissiles( dropship )
+ dropship.Hide()
+ EmitSoundOnEntity( dropship, dropshipSound ) //HACK: Note that the anims can play sounds too! For R3 just make it consistent so it's all played in script or all played in anims
+ thread ShowDropship( dropship )
+ thread PlayAnimTeleport( dropship, animation, ref, 0 )
+
+ ArrayRemoveDead( guys )
+
+ Signal( Table, "OnDropoff", { guys = guys } )
+
+ WaittillAnimDone( dropship )
+ wait 2.0
+}
+
+void function FindDropshipZiplineNodes( DropTable dropTable, FlightPath flightPath, vector origin, vector angles, string side = "both", bool ignoreCollision = false, bool amortize = false )
+{
+ dropTable.nodes = DropshipFindDropNodes( flightPath, origin, angles.y, side, ignoreCollision, IsLegalFlightPath_OverTime, amortize )
+ dropTable.valid = true
+}
+
+function ShowDropship( dropship )
+{
+ dropship.EndSignal( "OnDestroy" )
+ wait 0.16
+ dropship.Show()
+}
+
+entity function AddTurret( entity dropship, int team, string turretWeapon, string attachment, int health = 700 )
+{
+ entity turret = CreateEntity( "npc_turret_sentry" )
+ turret.kv.TurretRange = 1500
+ turret.kv.AccuracyMultiplier = 1.0
+ turret.kv.FieldOfView = 0.4
+ turret.kv.FieldOfViewAlert = 0.4
+ SetSpawnOption_Weapon( turret, turretWeapon )
+ turret.SetOrigin( Vector(0,0,0) )
+ turret.SetTitle( "#NPC_DROPSHIP" )
+ turret.s.skipTurretFX <- true
+ DispatchSpawn( turret )
+
+ SetTargetName( turret, "DropshipTurret" )
+ turret.SetHealth( health)
+ turret.SetMaxHealth( health )
+ turret.Hide()
+ //turret.Show()
+ entity weapon = turret.GetActiveWeapon()
+ weapon.Hide()
+ SetTeam( turret, team )
+ turret.SetParent( dropship, attachment, false )
+ turret.EnableTurret()
+ turret.SetOwner( dropship.GetOwner() )
+ turret.SetAimAssistAllowed( false )
+
+ entity bossPlayer = dropship.GetBossPlayer()
+ if ( IsValidPlayer( bossPlayer ) )
+ turret.SetBossPlayer( dropship.GetBossPlayer() )
+
+ HideName( turret )
+ return turret
+}
+
+function GetTeamDropshipSound( team, animation )
+{
+ Assert( team in file.dropshipSound )
+ Assert( animation in file.dropshipSound[ team ] )
+
+ return file.dropshipSound[ team ][ animation ]
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_lasermesh.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_lasermesh.gnut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_lasermesh.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_loadout_crate.nut b/Northstar.CustomServers/scripts/vscripts/mp/_loadout_crate.nut
new file mode 100644
index 000000000..d987c774c
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_loadout_crate.nut
@@ -0,0 +1,183 @@
+untyped
+
+global function LoadoutCrate_Init
+
+const LOADOUT_CRATE_MODEL = $"models/containers/pelican_case_ammobox.mdl"
+global function AddLoadoutCrate
+global function DestroyAllLoadoutCrates
+
+function LoadoutCrate_Init()
+{
+ level.loadoutCrateManagedEntArrayID <- CreateScriptManagedEntArray()
+ PrecacheModel( LOADOUT_CRATE_MODEL )
+
+ AddSpawnCallback( "prop_dynamic", LoadoutCreatePrePlaced )
+}
+
+function AddLoadoutCrate( team, vector origin, vector angles, bool showOnMinimap = true, entity crate = null )
+{
+ expect int( team )
+
+ local crateCount = GetScriptManagedEntArray( level.loadoutCrateManagedEntArrayID ).len()
+ Assert( crateCount < MAX_LOADOUT_CRATE_COUNT, "Can't have more than " + MAX_LOADOUT_CRATE_COUNT + " Loadout Crates" )
+
+ angles += Vector( 0, -90, 0 )
+
+ if ( !IsValid( crate ) )
+ {
+ crate = CreatePropScript( LOADOUT_CRATE_MODEL, origin, angles, 6 )
+ SetTargetName( crate, "loadoutCrate" )
+ }
+
+ SetTeam( crate, team )
+ crate.SetUsable()
+ if ( team == TEAM_MILITIA || team == TEAM_IMC )
+ crate.SetUsableByGroup( "friendlies pilot" )
+ else
+ crate.SetUsableByGroup( "pilot" )
+
+ crate.SetUsePrompts( "#LOADOUT_CRATE_HOLD_USE", "#LOADOUT_CRATE_PRESS_USE" )
+ crate.SetForceVisibleInPhaseShift( true )
+
+ if ( showOnMinimap )
+ {
+ #if R1_VGUI_MINIMAP
+ crate.Minimap_SetDefaultMaterial( $"vgui/hud/coop/coop_ammo_locker_icon" )
+ #endif
+ crate.Minimap_SetObjectScale( MINIMAP_LOADOUT_CRATE_SCALE )
+ crate.Minimap_SetAlignUpright( true )
+ crate.Minimap_AlwaysShow( TEAM_IMC, null )
+ crate.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ crate.Minimap_SetHeightTracking( true )
+ crate.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ crate.Minimap_SetCustomState( eMinimapObject_prop_script.FD_LOADOUT_CHEST )
+ }
+
+ AddToScriptManagedEntArray( level.loadoutCrateManagedEntArrayID, crate )
+
+ //thread LoadoutCrateMarkerThink( "LoadoutCrateMarker" + string( crateCount ), crate )
+ thread LoadoutCrateThink( crate )
+ thread LoadoutCrateRestockAmmoThink( crate )
+
+ Highlight_SetNeutralHighlight( crate, "interact_object_los" )
+}
+
+void function LoadoutCreatePrePlaced( entity ent )
+{
+ if ( ent.GetTargetName().find( "loot_crate" ) == 0 )
+ {
+ ent.Destroy()
+ return
+ }
+
+ if ( ent.GetTargetName().find( "loadout_crate" ) != 0 )
+ return
+
+ if ( IsSingleplayer() )
+ return
+
+ vector angles = ent.GetAngles() + Vector( 0, 90, 0 )
+ AddLoadoutCrate( TEAM_BOTH, ent.GetOrigin(), angles, false, ent )
+}
+
+function LoadoutCrateMarkerThink( marker, crate )
+{
+ crate.EndSignal( "OnDestroy" )
+ crate.EndSignal( "OnDeath" )
+
+ OnThreadEnd(
+ function() : ( marker )
+ {
+ ClearMarker( marker )
+ }
+ )
+
+ while ( 1 )
+ {
+ if ( GetGameState() <= eGameState.Prematch )
+ ClearMarker( marker )
+ else
+ SetMarker( marker, crate )
+
+ svGlobal.levelEnt.WaitSignal( "GameStateChanged" )
+ }
+}
+
+function LoadoutCrateThink( crate )
+{
+ crate.EndSignal( "OnDestroy" )
+ while ( true )
+ {
+ var player = crate.WaitSignal( "OnPlayerUse" ).player
+
+ if ( player.IsPlayer() )
+ {
+ thread UsingLoadoutCrate( crate, player )
+ wait 1 // debounce on using the crate to minimize the risk of using it twice before the menu opens.
+ }
+ }
+}
+
+function LoadoutCrateRestockAmmoThink( crate )
+{
+ crate.EndSignal( "OnDestroy" )
+ local distSqr
+ local crateOrigin = crate.GetOrigin()
+ local triggerDistSqr = 96 * 96
+ local resetDistSqr = 384 * 384
+
+ while ( true )
+ {
+ wait 1 // check every second
+ array<entity> playerArray = GetPlayerArray_Alive()
+ foreach( player in playerArray )
+ {
+ if ( player.IsTitan() )
+ continue
+
+ if ( player.ContextAction_IsBusy() )
+ continue
+
+ distSqr = DistanceSqr( crateOrigin, player.GetOrigin() )
+ if ( distSqr <= triggerDistSqr && player.s.restockAmmoTime < Time() )
+ {
+ if ( TraceLineSimple( player.EyePosition(), crate.GetOrigin() + Vector( 0.0, 0.0, 24.0 ), crate ) == 1.0 )
+ {
+ player.s.restockAmmoCrate = crate
+ player.s.restockAmmoTime = Time() + 10 // debounce time before you can get new ammo again if you stay next to the crate.
+ //MessageToPlayer( player, eEventNotifications.CoopAmmoRefilled, null, null )
+ RestockPlayerAmmo( player )
+ }
+ }
+
+ if ( distSqr > resetDistSqr && player.s.restockAmmoTime > 0 && player.s.restockAmmoCrate == crate )
+ {
+ player.s.restockAmmoCrate = null
+ player.s.restockAmmoTime = 0
+ }
+ }
+ }
+}
+
+function UsingLoadoutCrate( crate, player )
+{
+ expect entity( player )
+
+ player.p.usingLoadoutCrate = true
+ player.s.usedLoadoutCrate = true
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Coop_AmmoBox_Open" )
+ Remote_CallFunction_UI( player, "ServerCallback_OpenPilotLoadoutMenu" )
+}
+
+// should be called if we enter an epilogue ... maybe?
+function DestroyAllLoadoutCrates()
+{
+ local crateArray = GetScriptManagedEntArray( level.loadoutCrateManagedEntArrayID )
+ foreach( crate in crateArray )
+ crate.Destroy()
+
+ //dissolve didn't work
+ //Dissolve( ENTITY_DISSOLVE_CHAR, Vector( 0, 0, 0 ), 0 )
+ //ENTITY_DISSOLVE_CORE
+ //ENTITY_DISSOLVE_NORMAL
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_mp_mapspawn.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_mp_mapspawn.gnut
new file mode 100644
index 000000000..6860d8176
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_mp_mapspawn.gnut
@@ -0,0 +1,65 @@
+//todo change this to be map-based
+global function SPMP_MapSpawn_Init
+global struct SvSpawnGlobals
+{
+ array<entity> allNormalSpawnpoints
+}
+
+global SvSpawnGlobals svSpawnGlobals
+
+void function SPMP_MapSpawn_Init()
+{
+ printl( "Code Script: _mp_mapspawn" )
+
+ svGlobal.npcsSpawnedThisFrame_scriptManagedArray[ TEAM_IMC ] <- CreateScriptManagedEntArray()
+ svGlobal.npcsSpawnedThisFrame_scriptManagedArray[ TEAM_MILITIA ] <- CreateScriptManagedEntArray()
+
+ level.spawnActions <- {} // these run after all initial spawn functions have run
+ svGlobal.levelEnt = CreateEntity( "info_landmark" )
+ SetTargetName( svGlobal.levelEnt, "Level Ent" )
+ DispatchSpawn( svGlobal.levelEnt )
+ level.isTestmap <- false
+
+ FlagInit( "EntitiesDidLoad" )
+ FlagInit( "PlayerDidSpawn" )
+
+ level.privateMatchForcedEnd <- null
+ level.defenseTeam <- TEAM_IMC
+
+ level.onRodeoStartedCallbacks <- [] // runs when a player starts rodeoing a titan
+ level.onRodeoEndedCallbacks <- [] // runs when a player stops rodeoing a titan
+
+ FlagInit( "FireteamAutoSpawn" )
+ FlagInit( "DebugFoundEnemy" )
+ FlagInit( "OldAnimRefStyle" )
+ FlagInit( "EarlyCatch" )
+ FlagInit( "ForceStartSpawn" )
+ FlagInit( "IgnoreStartSpawn" )
+ FlagInit( "ReadyToStartMatch" ) // past waiting for players, in prematch
+
+ RegisterSignal( "OnChangedPlayerClass" )
+ RegisterSignal( "Disconnected" )
+ RegisterSignal( "_disconnectedInternal" )
+ RegisterSignal( "TeamChange" )
+ RegisterSignal( "LeftClass" )
+ RegisterSignal( "forever" )
+ RegisterSignal( "waitOver" )
+ RegisterSignal( "HitSky" )
+
+ AddSpawnCallback( "trigger_hurt", InitDamageTriggers )
+
+ AddSpawnCallbackEditorClass( "func_brush", "func_brush_navmesh_separator", NavmeshSeparatorThink )
+
+ //AddCallback_EntitiesDidLoad( ActivateSkyBox )
+
+ AddSpawnCallback( "player", MP_PlayerPostInit )
+
+ // unsure if this should be done here, but it's required for mp to load
+ PrecacheModel( $"models/menu/default_environment.mdl" )
+
+ //if ( IsMultiplayer() && GetClassicMPMode() && !IsLobby() )
+ // ClassicMP_TryDefaultIntroSetup()
+
+ //InitDefaultLoadouts()
+ SPMP_Shared_Init()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_music.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_music.gnut
new file mode 100644
index 000000000..443203361
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_music.gnut
@@ -0,0 +1,107 @@
+global function Music_Init
+global function CreateTeamMusicEvent
+global function PlayCurrentTeamMusicEventsOnPlayer
+global function CreateLevelIntroMusicEvent
+global function PlayMusicToCompletion
+global function PlayMusicToAll
+global function CreateLevelWinnerDeterminedMusicEvent
+
+const int MUSIC_EVENT_UNINITIALIZED = -1
+
+
+struct MusicEvent
+{
+ int musicPieceID = MUSIC_EVENT_UNINITIALIZED
+ float timeMusicStarted
+ bool shouldSeek
+}
+
+struct
+{
+ table< int, MusicEvent > musicEvents
+} file
+
+
+void function Music_Init()
+{
+ MusicEvent imcMusicEvent
+ MusicEvent militiaMusicEvent
+ file.musicEvents[ TEAM_IMC ] <- imcMusicEvent
+ file.musicEvents[ TEAM_MILITIA ] <- militiaMusicEvent
+
+ AddCallback_GameStateEnter( eGameState.Prematch, CreateLevelIntroMusicEvent )
+}
+
+void function CreateTeamMusicEvent( int team, int musicPieceID, float timeMusicStarted, bool shouldSeek = true )
+{
+ Assert( !( shouldSeek == false && timeMusicStarted > 0 ), "Don't pass in timeMusicStarted when creating a TeamMusicEvent with shouldSeek set to false!" )
+
+ MusicEvent musicEvent
+ musicEvent.musicPieceID = musicPieceID
+ musicEvent.timeMusicStarted = timeMusicStarted
+ musicEvent.shouldSeek = shouldSeek
+
+ file.musicEvents[ team ] = musicEvent
+}
+
+void function PlayCurrentTeamMusicEventsOnPlayer( entity player )
+{
+ int team = player.GetTeam()
+ MusicEvent musicEvent
+
+ if ( team in file.musicEvents )
+ musicEvent = file.musicEvents[ team ]
+ else
+ musicEvent = file.musicEvents[ TEAM_MILITIA ] //This normally means we're in FFA. Fine to failsafe to use any music event
+
+ if ( musicEvent.musicPieceID == MUSIC_EVENT_UNINITIALIZED ) //No current music event
+ return
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayTeamMusicEvent", musicEvent.musicPieceID, musicEvent.timeMusicStarted, musicEvent.shouldSeek )
+}
+
+void function CreateLevelIntroMusicEvent()
+{
+ //printt( "Creating LevelIntroMusicEvent" )
+ CreateTeamMusicEvent( TEAM_IMC, eMusicPieceID.LEVEL_INTRO, Time() )
+ CreateTeamMusicEvent( TEAM_MILITIA, eMusicPieceID.LEVEL_INTRO, Time() )
+}
+
+void function PlayMusicToCompletion( int musicID )
+{
+ array<entity> players = GetPlayerArray()
+ foreach ( entity player in players )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayMusicToCompletion", musicID )
+ }
+}
+
+void function PlayMusicToAll( int musicID )
+{
+ array<entity> players = GetPlayerArray()
+ foreach ( entity player in players )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_PlayMusic", musicID )
+ }
+}
+
+void function CreateLevelWinnerDeterminedMusicEvent()
+{
+ //printt( "Creating CreateLevelWinnerDeterminedMusicEvent" )
+ if ( IsFFAGame() )
+ return
+
+ int winningTeam = GetWinningTeam()
+
+ if ( winningTeam )
+ {
+ int losingTeam = GetOtherTeam( winningTeam )
+ CreateTeamMusicEvent( winningTeam, eMusicPieceID.LEVEL_WIN, Time() )
+ CreateTeamMusicEvent( losingTeam, eMusicPieceID.LEVEL_LOSS, Time() )
+ }
+ else
+ {
+ CreateTeamMusicEvent( TEAM_MILITIA, eMusicPieceID.LEVEL_DRAW, Time() )
+ CreateTeamMusicEvent( TEAM_IMC, eMusicPieceID.LEVEL_DRAW, Time() )
+ }
+}
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_pickups.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_pickups.gnut
new file mode 100644
index 000000000..ecf9b3e51
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_pickups.gnut
@@ -0,0 +1,1195 @@
+untyped
+
+global function Pickups_Init
+global function AddCollectible
+global function UpdateCollectiblesAfterLoadSaveGame
+global function CreateWeaponPickup
+global function CreatePickup
+global function WaitUntilPlayerPicksUp
+global function AddClipToWeapon
+global function AddRoundsToWeapon
+global function AddRoundToWeapon
+global function AddClipToMainWeapons
+global function AddTwoClipToMainWeapons
+global function AddRoundToOrdnance
+global function AddRoundsToTactical
+global function CreateScriptWeapon
+global function GetAllLeveledScriptWeapons
+global function TitanLoadoutWaitsForPickup // Needed only to be global to spawn pickups from dev menu.
+
+#if DEV
+ global function Dev_ResetCollectiblesProgress_Level
+#endif
+
+const HEALTH_PICKUP_AMOUNT = 3000
+global const PICKUP_GLOW_FX = $"P_ar_titan_droppoint"
+//const HEALTH_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam_collision.mdl"
+//const HEALTH_MODEL = $"models/domestic/trash_can_green_closed.mdl"
+//const HEALTH_MODEL = $"models/weapons/bullets/projectile_rocket_large.mdl"
+const HEALTH_MODEL = $"models/gameplay/health_pickup_small.mdl"
+const HEALTH_MODEL_LARGE = $"models/gameplay/health_pickup_large.mdl"
+
+const GRENADE_AMMO_MODEL = $"models/Weapons/ammoboxes/ammobox_01.mdl"
+const LION_MODEL = $"models/statues/lion_statue_bronze_green_small.mdl"
+const HELMET_COLLECTIBLE_MODEL = $"models/humans/heroes/mlt_hero_jack_helmet_static.mdl"
+const COLLECTIBLE_GLOW_EFFECT = $"P_item_bluelion"
+
+
+global struct LeveledScriptedWeapons
+{
+ table<string, bool> foundScriptWeapons
+ array<entity> infoTargets
+}
+
+struct HealthPickup
+{
+ float healAmount
+ float healTime
+ string pickupSound
+ string healSound
+ string endSound
+ asset model
+}
+
+struct Collectible
+{
+ entity ent
+ int id
+ vector pos
+}
+
+struct
+{
+ int nextHealthDropSmall
+ int nextHealthDropLarge
+ float lastHealthDropTime
+ table<string, HealthPickup> healthPickups
+ array<Collectible> collectibles
+ int testMapCollectibleValue
+} file
+
+function Pickups_Init()
+{
+ HealthPickup small
+ small.healAmount = 0.4
+ small.healTime = 1.0
+ small.pickupSound = "Pilot_HealthPack_Small_Pickup"
+ small.healSound = "Pilot_HealthPack_Small_Healing"
+ small.endSound = "Pilot_HealthPack_Small_Healing_End"
+ small.model = HEALTH_MODEL
+ file.healthPickups[ "health_pickup_small" ] <- small
+
+ HealthPickup large
+ large.healAmount = 0.8
+ large.healTime = 2.0
+ large.pickupSound = "Pilot_HealthPack_Large_Pickup"
+ large.healSound = "Pilot_HealthPack_Large_Healing"
+ large.endSound = "Pilot_HealthPack_Large_Healing_End"
+ large.model = HEALTH_MODEL_LARGE
+ file.healthPickups[ "health_pickup_large" ] <- large
+
+
+ //AddSpawnCallbackEditorClass( "script_ref", "script_pickup_health", HealthPickup_OnSpawned )
+ //AddSpawnCallbackEditorClass( "script_ref", "script_pickup_health_large", HealthPickupLarge_OnSpawned )
+ AddSpawnCallbackEditorClass( "script_mover_lightweight", "script_collectible", AddCollectible )
+ AddSpawnCallbackEditorClass( "script_ref", "script_pickup_weapon", CreateWeaponPickup )
+ //AddSpawnCallbackEditorClass( "script_ref", "script_pickup_grenades", CreateGrenadeAmmoPickup )
+ //AddSpawnCallbackEditorClass( "script_ref", "script_pickup_ammo", CreateGrenadeAmmoPickup )
+ AddSpawnCallbackEditorClass( "script_ref", "script_pickup_titan", CreateTitanPickup )
+
+ PrecacheModel( HEALTH_MODEL )
+ PrecacheModel( HEALTH_MODEL_LARGE )
+ PrecacheModel( GRENADE_AMMO_MODEL )
+ PrecacheModel( LION_MODEL )
+ PrecacheModel( HELMET_COLLECTIBLE_MODEL )
+ PrecacheParticleSystem( COLLECTIBLE_PICKUP_EFFECT )
+ PrecacheParticleSystem( COLLECTIBLE_GLOW_EFFECT )
+
+ RegisterSignal( "NewHealthPickup" )
+ RegisterSignal( "CollectibleEndThink" )
+
+ SetNextHealthDropSmall()
+ SetNextHealthDropLarge()
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad_Pickups )
+}
+
+void function CreateScriptWeapon( entity point )
+{
+ CreateWeaponPickup( point )
+ array<entity> linkParents = point.GetLinkParentArray()
+ foreach ( entity linkParent in linkParents )
+ {
+ linkParent.UnlinkFromEnt( point )
+ }
+ //point.Destroy()
+}
+
+void function EntitiesDidLoad_Pickups()
+{
+ if ( shGlobal.proto_pilotHealthPickupsEnabled )
+ {
+ AddDeathCallback( "npc_soldier", OnNPCKilled_DropHealth )
+ AddDeathCallback( "npc_spectre", OnNPCKilled_DropHealth )
+ }
+
+ SetupCollectibles()
+}
+
+void function SetNextHealthDropSmall()
+{
+ if ( shGlobal.proto_pilotHealthRegenDisabled )
+ file.nextHealthDropSmall = RandomInt( 6 ) + 6
+ else
+ file.nextHealthDropSmall = RandomInt( 4 ) + 4
+
+ file.lastHealthDropTime = Time()
+}
+
+void function SetNextHealthDropLarge()
+{
+ file.nextHealthDropLarge = RandomIntRange( 3, 6 )
+}
+
+void function OnNPCKilled_DropHealth( entity npc, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsValid( attacker ) )
+ return
+
+ switch ( npc.GetClassName() )
+ {
+ case "npc_soldier":
+ case "npc_spectre":
+ case "npc_stalker":
+ break
+
+ default:
+ return
+ }
+
+ OnNPCKilled_DropHealth_Internal( npc, attacker, damageInfo )
+}
+
+void function OnNPCKilled_DropHealth_Internal( entity npc, entity attacker, var damageInfo )
+{
+ if ( npc.GetTeam() != TEAM_IMC )
+ return
+
+ file.nextHealthDropSmall--
+
+ if ( !DropHealthFromDeath( npc, attacker ) )
+ return
+
+ SetNextHealthDropSmall()
+
+ vector angles = npc.GetAngles()
+ angles.x = 0
+
+ vector forward = AnglesToForward( angles )
+
+ vector origin = npc.GetWorldSpaceCenter() + forward * 16
+
+ entity health
+ file.nextHealthDropLarge--
+ if ( file.nextHealthDropLarge < 0 && shGlobal.proto_pilotHealthRegenDisabled )
+ {
+ SetNextHealthDropLarge()
+ health = CreateHealthPickupSized( origin, angles, "health_pickup_large" )
+ }
+ else
+ {
+ health = CreateHealthPickupSized( origin, angles, "health_pickup_small" )
+ }
+
+ EmitSoundOnEntity( health, "Pilot_HealthPack_Drop" )
+ health.Fire( "Kill", "", 180 )
+}
+
+bool function DropHealthFromDeath( entity npc, entity attacker )
+{
+ if ( !attacker.IsPlayer() )
+ return file.nextHealthDropSmall < 0
+
+ if ( Time() - file.lastHealthDropTime < 3.0 )
+ return false
+
+ float healthRatio = float( attacker.GetHealth() ) / attacker.GetMaxHealth()
+
+ float chanceFromNextHealth = GraphCapped( file.nextHealthDropSmall, 0, 5, 0.4, 1.1 )
+ float chanceFromLowHealth = GraphCapped( healthRatio, 0.25, 0.80, 0.4, 1.1 )
+ float dropChance = chanceFromNextHealth * chanceFromLowHealth
+
+ return RandomFloat( 1.0 ) > dropChance
+
+ /*
+ if ( file.nextHealthDropSmall <= 1 )
+ {
+ float ratio = float( attacker.GetHealth() ) / attacker.GetMaxHealth() - 0.15
+ float random = RandomFloat( 1.0 )
+ if ( random > ratio )
+ return true
+ }
+
+ if ( Time() - file.lastHealthDropTime < 5.0 )
+ return false
+
+ if ( float( attacker.GetHealth() ) / attacker.GetMaxHealth() > 0.25 )
+ return false
+
+ return Distance( attacker.GetOrigin(), npc.GetOrigin() ) < 2000
+ */
+}
+
+void function CreateTitanPickup( entity ent )
+{
+ entity mover = CreatePickup( ent, LION_MODEL, DropTitanPickedUp )
+
+ mover.EndSignal( "OnDestroy" )
+ wait 0.2 // for some buggy reason!?
+ EmitSoundOnEntity( mover, "health_pickup_loopsound_far" )
+ EmitSoundOnEntity( mover, "health_pickup_loopsound_near" )
+ return
+}
+
+bool function DropTitanPickedUp( entity player )
+{
+ if ( player.IsTitan() )
+ return false
+
+ AddPlayerScore( player, "PilotHealthPickup" )
+ EmitSoundOnEntity( player, "titan_energyshield_up" )
+ player.SetNextTitanRespawnAvailable( 0 )
+
+ return true
+}
+
+function DisplayTempNameText( entity ent, string text )
+{
+ ent.EndSignal( "OnDestroy" )
+ for ( ;; )
+ {
+ array<entity> players = GetPlayerArray()
+ if ( players.len() )
+ {
+ entity nearestPlayer = GetClosest( players, ent.GetOrigin(), 2300 )
+ if ( nearestPlayer != null )
+ DebugDrawText( ent.GetWorldSpaceCenter(), text, true, 1 )
+ }
+ wait 0.9
+ }
+}
+
+entity function CreateHealthPickupSized( vector origin, vector angles, string healthType )
+{
+ HealthPickup pickup = file.healthPickups[ healthType ]
+ entity ent = CreatePropPhysics( pickup.model, origin, angles )
+ ent.NotSolid()
+
+
+ angles = AnglesCompose( angles, < -45,0,0> )
+ vector forward = AnglesToForward( angles )
+ ent.SetVelocity( forward * 200 )
+
+ ent.SetAngularVelocity( RandomFloatRange( 300, 500 ), RandomFloatRange( -100, 100 ), 0 )
+
+ thread HealthPickupWaitsForPickup( ent, pickup )
+ Highlight_SetNeutralHighlight( ent, "health_pickup" )
+ return ent
+}
+
+void function HealthPickup_OnSpawned( entity ent )
+{
+ //if ( shGlobal.proto_pilotHealthRegenDisabled )
+ {
+ CreateHealthPickupSized( ent.GetOrigin(), ent.GetAngles(), "health_pickup_small" )
+ ent.Destroy()
+ return
+ }
+
+}
+
+void function HealthPickupLarge_OnSpawned( entity ent )
+{
+ if ( shGlobal.proto_pilotHealthRegenDisabled )
+ {
+ CreateHealthPickupSized( ent.GetOrigin(), ent.GetAngles(), "health_pickup_small" )
+ ent.Destroy()
+ //HealthPickup_OnSpawned( ent )
+ return
+ }
+
+ CreateHealthPickupSized( ent.GetOrigin(), ent.GetAngles(), "health_pickup_large" )
+ ent.Destroy()
+}
+
+void function CreateHealthRegenField( entity ent )
+{
+ ent.EndSignal( "OnDestroy" )
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+ )
+
+ PickupGlow pickupGlow = CreatePickupGlow( ent, 0, 255, 0 )
+
+ float healthRemainingMax = 1000
+ float healthRemainingCurrent = healthRemainingMax
+ float useIncrement = 9
+ float refillRate = 0
+ float nextRegenTime = 0
+
+ bool available
+ entity player
+ entity lastPlayer
+
+ for ( ;; )
+ {
+ WaitFrame()
+
+ float ratio = healthRemainingCurrent / healthRemainingMax
+ int green = int( Graph( ratio, 0, 1, 0, 255 ) )
+ PickupGlow_SetColor( pickupGlow, 0, green, 0 )
+
+ if ( Time() > nextRegenTime )
+ {
+ healthRemainingCurrent = min( healthRemainingCurrent + refillRate, healthRemainingMax )
+ nextRegenTime = Time() + 1.2
+ }
+
+ if ( healthRemainingCurrent < useIncrement )
+ continue
+
+ player = GetHealthPickupPlayer( ent )
+
+ if ( lastPlayer != player )
+ {
+ if ( IsValid( lastPlayer ) )
+ {
+ //EmitSoundOnEntity( lastPlayer, "Pilot_Stimpack_Deactivate" )
+ StopSoundOnEntity( lastPlayer, "Pilot_Stimpack_Loop" )
+ }
+
+ if ( player != null )
+ {
+ // new player powers up
+ EmitSoundOnEntity( player, "Pilot_Stimpack_Activate" )
+ EmitSoundOnEntity( player, "Pilot_Stimpack_Loop" )
+ }
+
+ lastPlayer = player
+ }
+
+ if ( player == null )
+ continue
+
+ // recent damage reduces healing effect, so you cant abuse it
+ float recentDamage = TotalDamageOverTime_BlendedOut( player, 0.5, 5.0 )
+
+ // damage is ramped down based on how much damage was taken recently
+ float damageMod = GraphCapped( recentDamage, 0, 40, 1.0, 0.35 )
+ float healthGain = useIncrement * damageMod
+
+ healthRemainingCurrent -= healthGain
+ int newHealth = int( min( player.GetHealth() + healthGain, player.GetMaxHealth() ) )
+ player.SetHealth( newHealth )
+
+ nextRegenTime = Time() + 10
+ }
+}
+
+entity function GetHealthPickupPlayer( entity ent )
+{
+ // try to heal the player
+ entity player = GetPickupPlayer( ent )
+ if ( player == null )
+ return null
+ if ( !IsPilot( player ) )
+ return null
+ if ( player.GetHealth() >= player.GetMaxHealth() )
+ return null
+
+ return player
+}
+
+void function CreateGrenadeAmmoPickup( entity ent )
+{
+ thread DisplayTempNameText( ent, "Ammo" )
+ CreatePickup( ent, GRENADE_AMMO_MODEL, GenericAmmoPickup )
+ CreatePickupGlow( ent, 13, 104, 255 )
+}
+
+entity function CreatePickup( entity ent, asset model, bool functionref( entity ) pickupFunc )
+{
+ entity mover = CreateEntity( "script_mover" )
+ mover.kv.solid = 0
+ mover.SetValueForModelKey( model )
+ mover.SetFadeDistance( 5000 )
+ mover.kv.SpawnAsPhysicsMover = 0
+ mover.SetOrigin( ent.GetOrigin() )
+ mover.SetAngles( ent.GetAngles() )
+ DispatchSpawn( mover )
+
+ ent.EndSignal( "OnDestroy" )
+
+ mover.SetOwner( ent )
+
+ ent.SetParent( mover )
+ thread PickupWaitsForPickup( ent, mover, pickupFunc )
+
+ return mover
+}
+
+void function PickupWaitsForPickup( entity ent, entity mover, bool functionref( entity ) pickupFunc )
+{
+ ent.EndSignal( "OnDestroy" )
+ OnThreadEnd(
+ function() : ( mover, ent )
+ {
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+ )
+
+// local colorVec = Vector(r,g,b)
+// local cpoint = CreateEntity( "info_placement_helper" )
+// SetTargetName( cpoint, UniqueString( "pickup_controlpoint" ) )
+// DispatchSpawn( cpoint )
+// cpoint.SetOrigin( colorVec )
+// local glowFX = PlayFXWithControlPoint( PICKUP_GLOW_FX, mover.GetOrigin(), cpoint, null, null, null, C_PLAYFX_LOOP )
+//
+// OnThreadEnd(
+// function() : ( ent, mover, glowFX, cpoint )
+// {
+// cpoint.Fire( "Kill", "", 1.0 )
+// if ( IsValid(glowFX) )
+// {
+// glowFX.Fire( "StopPlayEndCap" )
+// glowFX.Fire( "Kill", "", 1.0 )
+// }
+// mover.Destroy()
+// ent.Destroy()
+// }
+// )
+
+ for ( ;; )
+ {
+ entity player = WaitUntilPlayerPicksUp( ent )
+ if ( pickupFunc( player ) )
+ return
+ WaitFrame()
+ }
+}
+
+void function HealthPickupWaitsForPickup( entity ent, HealthPickup pickup )
+{
+ ent.EndSignal( "OnDestroy" )
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+ )
+
+ for ( ;; )
+ {
+ WaitFrame()
+
+ entity player = WaitUntilPlayerPicksUp( ent )
+ if ( player.IsTitan() )
+ continue
+ if ( player.GetHealth() >= player.GetMaxHealth() )
+ continue
+
+ thread HealPlayerOverTime( player, pickup )
+ return
+ }
+}
+
+
+void function TitanLoadoutWaitsForPickup( entity ent, bool functionref( entity, entity ) pickupFunc )
+{
+ ent.EndSignal( "OnDestroy" )
+ OnThreadEnd(
+ function() : ( ent )
+ {
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+ )
+
+ for ( ;; )
+ {
+ entity player = WaitUntilPlayerPicksUp( ent )
+ if ( pickupFunc( player, ent ) )
+ return
+ WaitFrame()
+ }
+}
+
+
+entity function GetPickupPlayer( entity ent )
+{
+ array<entity> players = GetPlayerArray()
+
+ vector entOrigin = ent.GetCenter()
+
+ foreach ( player in players )
+ {
+ if ( !IsAlive( player ) )
+ continue
+
+ int pickupDist
+ if ( player.IsTitan() )
+ pickupDist = 256 * 256
+ else
+ pickupDist = 96 * 96
+
+ if ( GetEditorClass( ent ) == "script_collectible" )
+ pickupDist = 72 * 72
+
+ vector playerOrigin = player.GetOrigin()
+ if ( DistanceSqr( playerOrigin, entOrigin ) < pickupDist )
+ {
+ TraceResults trace
+ trace = TraceLine( entOrigin, playerOrigin, [ player, ent ], TRACE_MASK_SOLID, TRACE_COLLISION_GROUP_NONE )
+ if ( trace.fraction >= 0.99 || trace.hitEnt == ent )
+ return player
+ }
+ }
+
+ return null
+}
+
+entity function WaitUntilPlayerPicksUp( entity ent )
+{
+ while ( true )
+ {
+ entity player = GetPickupPlayer( ent )
+ if ( player != null )
+ return player
+ WaitFrame()
+ }
+
+ unreachable
+}
+
+function PickupHover( mover )
+{
+ mover.EndSignal( "OnDestroy" )
+
+ int direction = 1
+
+ while ( 1 )
+ {
+ mover.MoveTo( mover.GetOrigin() + Vector( 0, 0, 20*direction ), 1, 0.4, 0.4 )
+ mover.RotateTo( mover.GetAngles() + Vector( 0, 90, 0 ), 1, 0, 0 )
+ direction *= -1
+ wait 1
+ }
+}
+
+int function AddClipToMainWeapons( entity player )
+{
+ int gainedAmmo
+ foreach ( weapon in player.GetMainWeapons() )
+ {
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ }
+ return gainedAmmo
+}
+
+int function AddTwoClipToMainWeapons( entity player )
+{
+ int gainedAmmo
+ for ( int i = 0; i < 2; i++ )
+ {
+ foreach ( weapon in player.GetMainWeapons() )
+ {
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ }
+ }
+ return gainedAmmo
+}
+
+int function AddRoundToOrdnance( entity player )
+{
+ int gainedAmmo
+ entity ordnance = player.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ if ( IsValid( ordnance ) )
+ gainedAmmo += AddRoundToWeapon( player, ordnance )
+ return gainedAmmo
+}
+
+int function AddRoundsToTactical( entity player, int count = 1 )
+{
+ int gainedAmmo
+ entity ordnance = player.GetOffhandWeapon( OFFHAND_SPECIAL )
+ if ( IsValid( ordnance ) )
+ gainedAmmo += AddRoundsToWeapon( player, ordnance, count )
+ return gainedAmmo
+}
+
+int function AddClipToWeapon( entity player, entity weapon )
+{
+ int ammoPerClip = weapon.GetWeaponPrimaryClipCountMax()
+ int gainedAmmo = 0
+
+ switch ( weapon.GetWeaponInfoFileKeyField( "fire_mode" ) )
+ {
+ case "offhand_hybrid":
+ case "offhand":
+ case "offhand_instant":
+
+ // offhand weapons typically cant store ammo, so refill the current clip
+ if ( ammoPerClip > 0 )
+ {
+ int primaryClipCount = weapon.GetWeaponPrimaryClipCount()
+ weapon.SetWeaponPrimaryClipCount( ammoPerClip )
+ gainedAmmo = weapon.GetWeaponPrimaryClipCount() - primaryClipCount
+ }
+ break
+
+ default:
+ int primaryAmmoCount = weapon.GetWeaponPrimaryAmmoCount()
+ // this weapon has off-clip ammo storage, so add ammo to storage
+ int stockpile = player.GetWeaponAmmoStockpile( weapon )
+ weapon.SetWeaponPrimaryAmmoCount( primaryAmmoCount + ammoPerClip )
+ gainedAmmo = player.GetWeaponAmmoStockpile( weapon ) - stockpile
+ break
+ }
+
+ return gainedAmmo
+}
+
+int function AddRoundToWeapon( entity player, entity weapon )
+{
+ return AddRoundsToWeapon( player, weapon, 1 )
+}
+
+int function AddRoundsToWeapon( entity player, entity weapon, int rounds )
+{
+ int ammoPerClip = weapon.GetWeaponPrimaryClipCountMax()
+ int gainedAmmo = 0
+
+ switch ( weapon.GetWeaponInfoFileKeyField( "fire_mode" ) )
+ {
+ case "offhand_hybrid":
+ case "offhand":
+ case "offhand_instant":
+
+ // offhand weapons typically cant store ammo, so refill the current clip
+ if ( ammoPerClip > 0 )
+ {
+ int primaryAmmoInClipCount = weapon.GetWeaponPrimaryClipCount()
+ int newAmmo = minint( ammoPerClip, primaryAmmoInClipCount + rounds )
+ if ( newAmmo > primaryAmmoInClipCount )
+ {
+ weapon.SetWeaponPrimaryClipCount( newAmmo )
+ gainedAmmo = weapon.GetWeaponPrimaryClipCount() - primaryAmmoInClipCount
+ }
+ }
+ break
+
+
+ default:
+ int primaryAmmoCount = weapon.GetWeaponPrimaryAmmoCount()
+ // this weapon has off-clip ammo storage, so add ammo to storage
+ int stockpile = player.GetWeaponAmmoStockpile( weapon )
+ weapon.SetWeaponPrimaryAmmoCount( primaryAmmoCount + rounds )
+ gainedAmmo = player.GetWeaponAmmoStockpile( weapon ) - stockpile
+ break
+ }
+
+ return gainedAmmo
+}
+
+bool function GenericAmmoPickup( entity player )
+{
+ if ( player.IsTitan() )
+ return false
+
+ Assert( player.IsPlayer() )
+
+ int gainedAmmo = 0
+ foreach ( weapon in player.GetMainWeapons() )
+ {
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ }
+ foreach ( weapon in player.GetOffhandWeapons() )
+ {
+ gainedAmmo += AddClipToWeapon( player, weapon )
+ }
+
+ if ( gainedAmmo > 0 )
+ {
+ AddPlayerScore( player, "PilotAmmoPickup" )
+ EmitSoundOnEntity( player, "BurnCard_GrenadeRefill_Refill" )
+ EmitSoundOnEntity( player, "titan_energyshield_up" )
+ return true
+ }
+
+ return false
+}
+
+
+bool function GrenadeAmmoPickedUp( entity player )
+{
+ if ( player.IsTitan() )
+ return false
+
+ Assert( player.IsPlayer() )
+
+ entity weapon = player.GetOffhandWeapon( 0 )
+ if ( !IsValid( weapon ) )
+ return false
+
+ int ammo = weapon.GetWeaponPrimaryClipCount()
+ int newAmmo = minint( player.GetWeaponAmmoMaxLoaded( weapon ), ammo + 2 )
+ weapon.SetWeaponPrimaryClipCount( newAmmo )
+
+ bool pickup = newAmmo > ammo
+
+ if ( pickup )
+ {
+ AddPlayerScore( player, "PilotAmmoPickup" )
+ EmitSoundOnEntity( player, "BurnCard_GrenadeRefill_Refill" )
+ EmitSoundOnEntity( player, "titan_energyshield_up" )
+ return true
+ }
+
+ return false
+}
+
+void function HealPlayerOverTime( entity player, HealthPickup pickup )
+{
+ //StimPlayer( player, pickup.healTime * 2.0 )
+
+ Assert( IsNewThread(), "Must be threaded off" )
+ Assert( player.IsPlayer() )
+ //AddPlayerScore( player, "PilotHealthPickup" )
+ //EmitSoundOnEntity( player, "titan_energyshield_up" )
+
+ // cycle old effects
+ //player.Signal( "NewHealthPickup" )
+ //player.EndSignal( "NewHealthPickup" )
+
+ Assert( IsAlive( player ) )
+ player.EndSignal( "OnDeath" )
+
+ EmitSoundOnEntity( player, pickup.pickupSound )
+ EmitSoundOnEntity( player, pickup.healSound )
+
+
+ int frames = 10 * int( pickup.healTime )
+ float amount = player.GetMaxHealth() * pickup.healAmount
+ float healthPerFrame = amount / frames
+ float midTime = Time() + pickup.healTime * 0.5
+ float healthRemainder = 0
+
+ for ( int i = 0; i < frames; i++ )
+ {
+ WaitFrame()
+
+ float healthThisFrame
+ if ( Time() < midTime )
+ healthThisFrame = healthPerFrame * 0.6
+ else
+ healthThisFrame = healthPerFrame * 1.4
+
+ healthRemainder += healthThisFrame % 1
+ healthThisFrame -= healthThisFrame % 1
+ healthThisFrame += ( healthRemainder - healthRemainder % 1 )
+ healthRemainder %= 1
+// printt( "healththisframe is " + healthThisFrame + " healthRemainder is " + healthRemainder )
+
+ float newHealth = min( player.GetHealth() + healthThisFrame, player.GetMaxHealth() )
+ player.SetHealth( newHealth )
+ }
+
+ EmitSoundOnEntity( player, pickup.endSound )
+}
+
+LeveledScriptedWeapons function GetAllLeveledScriptWeapons()
+{
+ LeveledScriptedWeapons leveledScriptedWeapons
+
+ table<string, bool> tableAllWeapons
+
+ foreach ( weaponName in GetAllSPWeapons() )
+ {
+ tableAllWeapons[ weaponName ] <- true
+ }
+
+ foreach ( ent in GetEntArrayByClass_Expensive( "info_target" ) )
+ {
+ if ( !ent.HasKey( "editorclass" ) )
+ continue
+
+ string editorclass = expect string( ent.kv.editorclass )
+ if ( !( editorclass in tableAllWeapons ) )
+ continue
+
+ leveledScriptedWeapons.infoTargets.append( ent )
+ leveledScriptedWeapons.foundScriptWeapons[ editorclass ] <- true
+ }
+
+ // legacy support
+ foreach ( ent in GetEntArrayByClass_Expensive( "script_ref" ) )
+ {
+ if ( !ent.HasKey( "editorclass" ) )
+ continue
+ if ( ent.kv.editorclass != "script_pickup_weapon" )
+ continue
+
+ Assert( ent.HasKey( "script_weapon" ) )
+ string weapon = expect string( ent.kv.script_weapon )
+ if ( !( weapon in tableAllWeapons ) )
+ continue
+
+ //leveledScriptedWeapons.infoTargets.append( ent )
+ leveledScriptedWeapons.foundScriptWeapons[ weapon ] <- true
+ }
+
+ AddSpawnCallbackEditorClass( "script_ref", "script_pickup_weapon", CreateWeaponPickup )
+
+ return leveledScriptedWeapons
+}
+
+void function CreateWeaponPickup( entity ent )
+{
+ Assert( ent.HasKey( "script_weapon" ) )
+ string weaponClass = ent.GetValueForKey( "script_weapon" )
+
+ #if DEV
+ if ( !IsTestMap() )
+ {
+ if ( !WeaponIsPrecached( weaponClass ) )
+ {
+ CodeWarning( "Weapon " + weaponClass + " is not precached, re-export auto precache script" )
+ return
+ }
+
+ if ( !GetWeaponInfoFileKeyField_Global( weaponClass, "leveled_pickup" ) )
+ {
+ CodeWarning( "Tried to place illegal " + weaponClass + " in leveled at " + ent.GetOrigin() )
+ return
+ }
+ }
+
+ VerifyWeaponPickupModel( ent, weaponClass )
+
+ if ( GetWeaponInfoFileKeyField_Global( weaponClass, "offhand_default_inventory_slot" ) == OFFHAND_LEFT )
+ {
+ CodeWarning( "Illegal pickup " + weaponClass + " at " + ent.GetOrigin() )
+ return
+ }
+
+ #endif
+
+
+ bool doMarkAsLoadoutPickup = false
+ int loadoutIndex = GetSPTitanLoadoutIndexForWeapon( weaponClass )
+ if ( loadoutIndex >= 0 )
+ {
+ if ( IsBTLoadoutUnlocked( loadoutIndex ) )
+ return
+ else
+ doMarkAsLoadoutPickup = true
+ }
+
+ bool constrain = !ent.HasKey( "start_constrained" ) || ( ent.HasKey( "start_constrained" ) && ent.GetValueForKey( "start_constrained" ) == "1" )
+ entity weapon
+
+ if ( constrain ) // make all weapons constrained for now
+ {
+ weapon = CreateWeaponEntityByNameConstrained( weaponClass, ent.GetOrigin(), ent.GetAngles() )
+ }
+ else
+ {
+ weapon = CreateWeaponEntityByNameWithPhysics( weaponClass, ent.GetOrigin(), ent.GetAngles() )
+ weapon.SetVelocity( <0,0,0> )
+ }
+
+ SetTargetName( weapon, "leveled_" + weaponClass )
+ if ( ent.HasKey( "fadedist" ) )
+ {
+ weapon.kv.fadedist = ent.kv.fadedist
+ }
+ else
+ {
+ weapon.kv.fadedist = -1
+ }
+
+ ApplyWeaponModsFromEnt( ent, weapon )
+
+ if ( ent.HasKey( "script_name" ) )
+ {
+ weapon.kv.script_name = ent.kv.script_name
+ }
+
+ if ( doMarkAsLoadoutPickup )
+ {
+ weapon.MarkAsLoadoutPickup()
+ thread CreateTitanWeaponPickupHintTrigger( weapon )
+ thread TitanLoadoutWaitsForPickup( weapon, SPTitanLoadoutPickup )
+ }
+
+ HighlightWeapon( weapon )
+
+ // for s2s -mo
+ // for sp_training (pickups travel with moving gun racks) -sean
+ if ( ent.GetParent() )
+ {
+ weapon.SetParent( ent.GetParent(), "", true )
+ }
+
+ // for sp_training, to replenish the weapon when it's picked up -sean
+ ent.e.attachedEnts.append( weapon )
+}
+
+void function VerifyWeaponPickupModel( entity ent, string weaponClass )
+{
+ var playermodel
+ var playermodel1 = GetWeaponInfoFileKeyFieldAsset_Global( weaponClass, "droppedmodel" )
+ var playermodel2 = GetWeaponInfoFileKeyFieldAsset_Global( weaponClass, "playermodel" )
+ if ( playermodel1 != $"" )
+ playermodel = playermodel1
+ else
+ playermodel = playermodel2
+ playermodel = playermodel.tolower()
+ expect asset( playermodel )
+
+ asset modelName = ent.GetModelName().tolower()
+ if ( modelName != $"" && playermodel != modelName )
+ CodeWarning( "Incorrect Model on weapon " + weaponClass + " at " + ent.GetOrigin() + ", replace with real weapon for auto fix. ( " + modelName + " != " + playermodel + ")" )
+}
+
+void function ApplyWeaponModsFromEnt( entity ent, entity weapon )
+{
+ if ( ent.HasKey( "script_mods" ) )
+ {
+ array<string> mods = split( ent.GetValueForKey( "script_mods" ), " " )
+ if ( mods.len() > 0 )
+ {
+ weapon.SetMods( mods )
+ return
+ }
+ }
+
+ array<string> mods = GetWeaponModsForCurrentLevel( weapon.GetWeaponClassName() )
+ if ( mods.len() )
+ {
+ weapon.SetMods( [ mods.getrandom() ] )
+ }
+}
+
+
+void function AddCollectible( entity ent )
+{
+ Assert( ent.GetClassName() == "script_mover_lightweight" )
+
+ if ( ent.GetModelName() != HELMET_COLLECTIBLE_MODEL )
+ ent.SetModel( HELMET_COLLECTIBLE_MODEL )
+
+ ent.DisableFastPathRendering() // Workaround for glow effect not drawing (bug #177177)
+
+ Collectible collectible
+ collectible.ent = ent
+ collectible.pos = ent.GetOrigin()
+ file.collectibles.append( collectible )
+
+ // Drop to ground
+ if ( !collectible.ent.HasKey( "hover" ) || collectible.ent.kv.hover == "0" )
+ {
+ vector groundPos = OriginToGround( collectible.ent.GetOrigin() + <0,0,1> )
+ collectible.ent.SetOrigin( groundPos + < 0, 0, 32 > )
+ collectible.pos = collectible.ent.GetOrigin()
+ }
+
+ // Effect and not solid
+ collectible.ent.DisableHibernation()
+ collectible.ent.NotSolid()
+ collectible.ent.EnableRenderAlways()
+ collectible.ent.kv.fadedist = 100000
+}
+
+void function SetupCollectibles()
+{
+ // Make sure that the number of collectibles in the level matches the hardcoded value in sh_consts so that SP menus know total number per level.
+ string mapName = GetMapName()
+ int saveIndex = GetCollectibleLevelIndex( mapName )
+ Assert( saveIndex < 0 || file.collectibles.len() == GetMaxLionsInLevel( mapName ), "Collectibles count mismatch. Update LEVEL_UNLOCKS_COUNT in sh_consts.gnut to " + file.collectibles.len() )
+
+ // Index the collectibles so each is unique and it's status can be stored in a cvar. They are sorted by distance from map center to keep consistent on each map load
+ file.collectibles.sort( SortCollectiblesFunc )
+
+ foreach ( int i, Collectible collectible in file.collectibles )
+ {
+ collectible.id = 1 << i
+ thread CollectibleThink( collectible )
+ }
+}
+
+int function SortCollectiblesFunc( Collectible a, Collectible b )
+{
+ float distA = DistanceSqr( a.ent.GetOrigin(), <0,0,0> )
+ float distB = DistanceSqr( b.ent.GetOrigin(), <0,0,0> )
+ if ( distA > distB )
+ return 1
+ else if ( distA < distB )
+ return -1
+ return 0
+}
+
+void function UpdateCollectiblesAfterLoadSaveGame()
+{
+ // This has to run when a save game is loaded because the collectible may be there in the save game, but it was picked up after the save, so we need to delete the ones the player picked up
+ foreach ( Collectible collectible in file.collectibles )
+ {
+ if ( HasCollectible( collectible ) )
+ {
+ //DebugDrawSphere( collectible.pos, 40.0, 255, 0, 0, true, 600.0 )
+ if ( IsValid( collectible.ent ) )
+ collectible.ent.Destroy()
+ Signal( collectible, "CollectibleEndThink" )
+ }
+ }
+
+ // Delete all the weapons already unlocked in the level
+ array<string> unlockedLoadouts = GetSPTitanLoadoutsUnlocked()
+ SPTitanLoadout_RemoveOwnedLoadoutPickupsInLevel( unlockedLoadouts )
+}
+
+#if DEV
+ void function Dev_ResetCollectiblesProgress_Level()
+ {
+ printt( "RESETTING COLLECTIBLE PROGRESS (LEVEL)" )
+ string mapName = GetMapName()
+ int saveIndex = GetCollectibleLevelIndex( mapName )
+ if ( saveIndex == -1 )
+ return
+ printt( " sp_unlocks_level_" + saveIndex, 0 )
+ SetConVarInt( "sp_unlocks_level_" + saveIndex, 0 )
+ }
+#endif
+
+void function CollectibleThink( Collectible collectible )
+{
+ EndSignal( collectible, "CollectibleEndThink" )
+
+ if ( HasCollectible( collectible ) )
+ {
+ //printt( "Player already has collectible", collectible.id )
+ //DebugDrawSphere( collectible.ent.GetOrigin(), 25.0, 150, 0, 0, true, 600.0 )
+ collectible.ent.Destroy()
+ return
+ }
+
+ entity glowEffect = StartParticleEffectOnEntity_ReturnEntity( collectible.ent, GetParticleSystemIndex( COLLECTIBLE_GLOW_EFFECT ), FX_PATTACH_ABSORIGIN_FOLLOW, 0 )
+
+ // Rotate the collectible
+ collectible.ent.NonPhysicsRotate( < 0, 0, 1 >, 35.0 )
+
+ WaitFrame() // emit sound doesn't work on first frame so we have to wait a frame so sound will play. Player can't pickup collectible in frame 1 anyways
+
+ EmitSoundOnEntity( collectible.ent, "Emit_PilotHelmet_Collectible" )
+
+ //wait 1.0
+ //DebugDrawText( collectible.ent.GetOrigin(), string(collectible.id), true, 600.0 )
+ //DebugDrawSphere( collectible.ent.GetOrigin(), 25.0, 255, 0, 0, true, 600.0 )
+
+ // Wait until it's touched by a player
+ entity player = WaitUntilPlayerPicksUp( collectible.ent )
+
+ // Remove collectible
+ EmitSoundOnEntity( player, "Pilot_Collectible_Pickup" )
+ if ( IsValid( glowEffect ) )
+ EffectStop( glowEffect )
+ collectible.ent.Destroy()
+
+ // Save to player profile
+ string mapName = GetMapName()
+ int saveIndex = GetCollectibleLevelIndex( mapName )
+ int bitMask
+ if ( saveIndex >= 0 )
+ {
+ // If it's a real map we store it to player profile
+ string unlockVar = "sp_unlocks_level_" + saveIndex
+ bitMask = GetConVarInt( unlockVar )
+ bitMask = bitMask | collectible.id
+ //printt( "Saving collectible state", unlockVar, bitMask )
+ SetConVarInt( unlockVar, bitMask )
+ }
+ else
+ {
+ // Not a real map, we store it to a file var that wont persist, just so we can pick them up and have kind of working collectibles in test maps
+ CodeWarning( "Collectible state not being saved because this map is not shipping" )
+ file.testMapCollectibleValue = file.testMapCollectibleValue | collectible.id
+ bitMask = file.testMapCollectibleValue
+ }
+
+ // See how many collectibles are found now to pass to the RUI
+ int numCollectiblesFound = GetCollectiblesFoundForLevel( mapName )
+ int maxCollectibles = GetMaxLionsInLevel( mapName )
+
+ // Show message on HUD
+ Remote_CallFunction_NonReplay( player, "ServerCallback_CollectibleFoundMessage", numCollectiblesFound, maxCollectibles )
+
+ CollectiblePickupRumble( player )
+
+ UpdateHeroStatsForPlayer( player )
+
+ int totalLionsCollectedForGame = GetTotalLionsCollected()
+
+ if ( totalLionsCollectedForGame >= GetTotalLionsInGame() )
+ UnlockAchievement( player, achievements.COLLECTIBLES_3 )
+
+ if ( totalLionsCollectedForGame >= ACHIEVEMENT_COLLECTIBLES_2_COUNT )
+ UnlockAchievement( player, achievements.COLLECTIBLES_2 )
+
+ if ( totalLionsCollectedForGame >= ACHIEVEMENT_COLLECTIBLES_1_COUNT )
+ UnlockAchievement( player, achievements.COLLECTIBLES_1 )
+}
+
+void function CollectiblePickupRumble( entity player )
+{
+ float rumbleAmplitude = 200.0
+ float rumbleFrequency = 90.0
+ float rumbleDuration = 2.2
+
+ CreateAirShakeRumbleOnly( player.GetOrigin(), rumbleAmplitude, rumbleFrequency, rumbleDuration )
+}
+
+bool function HasCollectible( Collectible collectible )
+{
+ string mapName = GetMapName()
+ int saveIndex = GetCollectibleLevelIndex( mapName )
+
+ // Not a shipping map, so there is no saved var for this level. Just always make it available
+ if ( saveIndex == -1 )
+ return false
+
+ string unlockVar = "sp_unlocks_level_" + saveIndex
+ int bitMask = GetConVarInt( unlockVar )
+
+ return bool(bitMask & collectible.id)
+}
+
+
+
+
+
+
+
+
+
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_pickups_glow.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_pickups_glow.gnut
new file mode 100644
index 000000000..f1fe4ecce
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_pickups_glow.gnut
@@ -0,0 +1,53 @@
+global function CreatePickupGlow
+global function PickupGlow_SetColor
+
+global struct PickupGlow
+{
+ entity cpoint
+ entity glowFX
+}
+
+PickupGlow function CreatePickupGlow( entity ent, int r, int g, int b )
+{
+ vector origin = ent.GetOrigin()
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "pickup_controlpoint" ) )
+ DispatchSpawn( cpoint )
+ cpoint.SetOrigin( Vector(r,g,b) )
+ entity glowFX = PlayFXWithControlPoint( COLLECTIBLE_PICKUP_EFFECT, origin, cpoint, -1, null, null, C_PLAYFX_LOOP )
+
+ PickupGlow pickupGlow
+ pickupGlow.cpoint = cpoint
+ pickupGlow.glowFX = glowFX
+ thread PickupGlowCleanup( ent, pickupGlow )
+ return pickupGlow
+}
+
+void function PickupGlowCleanup( entity ent, PickupGlow pickupGlow )
+{
+ OnThreadEnd(
+ function() : ( pickupGlow )
+ {
+ StopPickupGlow( pickupGlow )
+ }
+ )
+
+ ent.WaitSignal( "OnDestroy" )
+}
+
+void function StopPickupGlow( PickupGlow pickupGlow )
+{
+ if ( IsValid( pickupGlow.cpoint ) )
+ pickupGlow.cpoint.Destroy()
+
+ if ( IsValid(pickupGlow.glowFX) )
+ {
+ EntityFire( pickupGlow.glowFX, "StopPlayEndCap" )
+ pickupGlow.glowFX.Destroy()
+ }
+}
+
+void function PickupGlow_SetColor( PickupGlow pickupGlow, int r, int g, int b )
+{
+ pickupGlow.cpoint.SetOrigin( Vector( r, g, b ) )
+}
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_playlist.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_playlist.gnut
new file mode 100644
index 000000000..dfceab412
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_playlist.gnut
@@ -0,0 +1,6 @@
+global function Playlist_Init
+
+void function Playlist_Init()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_revive.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_revive.gnut
new file mode 100644
index 000000000..b2f5c4674
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_revive.gnut
@@ -0,0 +1,352 @@
+untyped
+
+
+global function Revive_Init
+
+global function PlayerRevivesOrBleedsOut
+global function DeathPackage_PlayerRevive
+global function ShouldRevivePlayer
+
+const float REVIVE_BLEED_OUT_TIME = 15.0
+global const float REVIVE_DEATH_TIME = 2.0
+const float REVIVE_DIST_OUTER = 135.0
+const float REVIVE_DIST_INNER = 120.0
+
+struct
+{
+ table fakePlayers
+} file
+
+function Revive_Init()
+{
+ if ( !ReviveEnabled() )
+ return
+
+ RegisterSignal( "KillReviveNag" )
+ RegisterSignal( "DoneBleedingOut" )
+ RegisterSignal( "ReviveSucceeded" )
+ RegisterSignal( "ReviveFailed" )
+ RegisterSignal( "ForceBleedOut" )
+
+ AddCallback_OnClientDisconnected( ReviveOnClientDisconnect )
+}
+
+void function PlayerRevivesOrBleedsOut( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "ForceBleedOut" )
+ svGlobal.levelEnt.EndSignal( "RoundEnd" )
+
+ table e = { revived = false }
+ //thread PlayerReviveVONag( player, 0.5 )
+
+ OnThreadEnd(
+ function() : ( player, e )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ player.Signal( "KillReviveNag" )
+ player.Signal( "DoneBleedingOut" )
+ player.nv.reviveBleedingOut = -1.0 //-1 means off
+
+ if ( e.revived )
+ {
+ player.Signal( "ReviveSucceeded")
+ thread PlayerStandsBackUp( player )
+ }
+ else
+ {
+ file.fakePlayers[ player ].Destroy()
+ player.Signal( "ReviveFailed" )
+ DecideRespawnPlayer( player )
+ }
+ }
+ )
+
+ wait( REVIVE_DEATH_TIME )
+ player.StartObserverMode( OBS_MODE_DEATHCAM )
+
+ ForceRespawnIfEntireTeamIsDead( player )
+
+ float endTime = Time() + REVIVE_BLEED_OUT_TIME
+ player.nv.reviveBleedingOut = endTime
+
+ bool reviving = false
+ float doneReviveTime = Time() + 100
+
+ float distOuterSqr = pow( REVIVE_DIST_OUTER, 2 )
+ float distInnerSqr = pow( REVIVE_DIST_INNER, 2 )
+
+ while ( true )
+ {
+ array<entity> healers = Revive_GetAvailablePlayerHealers( player )
+
+ //we were reviving but aren't anymore - set revive to false.
+ if ( reviving && !FriendlyIsReviving( healers, player, distOuterSqr ) )
+ {
+ //thread PlayerReviveVONag( player )
+ reviving = false
+ player.nv.reviveHealedTime = -1.0 //-1 means off
+ }
+ //we were not reviving but now we are? set the new revive done time.
+ else if ( !reviving && FriendlyIsReviving( healers, player, distInnerSqr ) )
+ {
+ player.Signal( "KillReviveNag" )
+ doneReviveTime = Time() + REVIVE_TIME_TO_REVIVE
+ player.nv.reviveHealedTime = doneReviveTime
+ reviving = true
+ }
+
+ //are we done reviving? then set the value and return
+ if ( reviving && Time() > doneReviveTime )
+ {
+ e.revived = true
+ return
+ }
+
+ //we didn't make it
+ if ( !reviving && Time() > endTime )
+ return
+
+ wait 0.2
+ }
+}
+
+void function ForceRespawnIfEntireTeamIsDead( entity player )
+{
+ int playerTeam = player.GetTeam()
+ array<entity> victimTeamMembers = GetPlayerArrayOfTeam( playerTeam )
+ foreach ( member in victimTeamMembers )
+ {
+ if ( member.p.isReviving || IsAlive( member ) )
+ return
+ }
+ foreach ( member in victimTeamMembers )
+ {
+ if ( player != member && member.p.isReviving == false )
+ member.Signal( "ForceBleedOut" )
+ }
+ MessageToTeam( GetOtherTeam( playerTeam ), eEventNotifications.EnemyTeamEliminated )
+ player.Signal( "ForceBleedOut" )
+}
+
+void function PlayerReviveVONag( entity player, float delay = 0.5 )
+{
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "KillReviveNag" )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ StopSoundOnEntity( player, "diag_coop_bleedout_help" )
+ }
+ )
+
+ if ( delay > 0 )
+ wait delay
+
+ while ( true )
+ {
+ float time = EmitSoundOnEntity( player, "diag_coop_bleedout_help" )
+ wait time
+
+ wait RandomFloatRange( 10, 15 )
+ }
+}
+
+bool function FriendlyIsReviving( array<entity> healers, entity player, float distSqr )
+{
+ vector origin = player.GetOrigin()
+
+ foreach ( friend in healers )
+ {
+ if ( !IsAlive( friend ) )
+ continue
+
+ if ( DistanceSqr( friend.GetOrigin(), origin ) < distSqr )
+ return true
+ }
+
+ return false
+}
+
+array<entity> function Revive_GetAvailablePlayerHealers( entity player )
+{
+ int team = player.GetTeam()
+ array<entity> players = GetPlayerArrayOfTeam( team )
+ array<entity> playersCanRevive = []
+ foreach ( player in players )
+ {
+ if ( !IsAlive( player ) )
+ continue
+
+ playersCanRevive.append( player )
+ }
+
+ return playersCanRevive
+}
+
+bool function ShouldRevivePlayer( entity player, var damageInfo )
+{
+ if ( !ReviveEnabled() )
+ return false
+
+ if ( !GamePlaying() )
+ return false
+
+ if ( player.ContextAction_IsMeleeExecution() )
+ return false
+
+ if ( player.IsTitan() )
+ return false
+
+ int source = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ if ( source == eDamageSourceId.fall ||
+ source == eDamageSourceId.submerged ||
+ source == eDamageSourceId.outOfBounds ||
+ source == eDamageSourceId.indoor_inferno )
+ return false
+
+ return true
+}
+
+entity function SpawnFakePlayer( entity player, int team, vector origin, vector angles, asset weaponModel, asset model )
+{
+ float fadeDist = 10000.0
+ int solidType = 0// 0 = no collision, 2 = bounding box, 6 = use vPhysics, 8 = hitboxes only
+
+ entity fakePlayer = CreatePropDynamic( model, origin, angles, solidType, fadeDist )
+ if ( !( player in file.fakePlayers ) )
+ {
+ file.fakePlayers[ player ] <- null
+ }
+ file.fakePlayers[ player ] = fakePlayer
+
+ thread FakePlayerTrack( fakePlayer, player )
+
+ if ( weaponModel != $"" )
+ {
+ entity gun = CreatePropDynamic( weaponModel, origin, angles, 0, fadeDist )
+ gun.SetParent( fakePlayer, "PROPGUN" )
+ }
+
+ return fakePlayer
+}
+
+void function FakePlayerTrack( entity fakePlayer, entity player )
+{
+ fakePlayer.EndSignal( "OnDestroy" )
+ player.EndSignal( "OnDestroy" )
+ vector lastPlayerOrg = Vector( 0.0, 0.0, 0.0 )
+
+ while ( true )
+ {
+ if ( player.GetOrigin() == lastPlayerOrg )
+ player.SetVelocity( Vector( 0.0, 0.0, 0.0 ) )
+ lastPlayerOrg = player.GetOrigin()
+
+ fakePlayer.SetOrigin( player.GetOrigin() )
+ WaitFrame()
+ }
+}
+
+void function DeathPackage_PlayerRevive( entity player )
+{
+ player.kv.VisibilityFlags = ENTITY_VISIBLE_TO_NOBODY
+
+ vector deathOrg = player.GetOrigin()
+
+ vector mins = Vector( -16.0, -16.0, 0.0 )
+ vector maxs = Vector( 16.0, 16.0, 72.0 )
+ TraceResults result = TraceHull( deathOrg + Vector( 0.0, 0.0, 8.0 ), deathOrg + Vector( 0.0, 0.0, -16000.0 ), mins, maxs, player, ( TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS ), TRACE_COLLISION_GROUP_NONE )
+
+ player.SetVelocity( Vector( 0.0, 0.0, 0.0 ) )
+ thread ReviveLerpToOrigin( player, deathOrg, result.endPos )
+
+ entity activeWeapon = player.GetActiveWeapon()
+
+ asset weaponModel = activeWeapon == null ? $"" : activeWeapon.GetModelName()
+
+ entity fakePlayer = SpawnFakePlayer( player, player.GetTeam(), deathOrg, player.GetAngles(), weaponModel, player.GetModelName() )
+ fakePlayer.Anim_Play( "pt_wounded_drag_zinger_A_idle" )
+ player.Anim_Play( "pt_wounded_drag_zinger_A_idle" )
+}
+
+void function ReviveLerpToOrigin( entity player, vector deathOrg, vector endPos )
+{
+ player.EndSignal( "DoneBleedingOut" )
+ player.EndSignal( "OnDestroy" )
+
+ entity mover = CreateScriptMover()
+
+ OnThreadEnd(
+ function() : ( player, mover )
+ {
+ if ( IsValid( player ) )
+ player.ClearParent()
+
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ }
+ )
+
+ mover.SetOrigin( deathOrg )
+ player.SetOrigin( deathOrg )
+ player.SetParent( mover )
+
+ float moveTime = GraphCapped( deathOrg.z - endPos.z, 0.0, 768.0, 0.1, 2.0 )
+
+ mover.NonPhysicsMoveTo( endPos, moveTime, moveTime, 0.0 )
+ wait( moveTime )
+ player.ClearParent()
+
+ while ( true )
+ {
+ player.SetOrigin( endPos )
+ WaitFrame()
+ }
+}
+
+void function PlayerStandsBackUp( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ svGlobal.levelEnt.EndSignal( "RoundEnd" )
+
+ entity fakePlayer = expect entity( file.fakePlayers[ player ] )
+ file.fakePlayers[ player ] = null
+
+ player.p.isReviving = true
+
+ OnThreadEnd(
+ function() : ( player, fakePlayer )
+ {
+ if ( IsValid( fakePlayer ) )
+ fakePlayer.Destroy()
+
+ if ( IsValid( player ) )
+ player.p.isReviving = false
+ }
+ )
+
+ fakePlayer.Anim_Play( "CQB_knockback_pain_react" )
+ fakePlayer.Anim_SetInitialTime( 2.0 )
+ wait( 1.5 )
+
+ var settings = player.GetPlayerSettings()
+ player.SetPlayerSettings( "spectator" )
+ player.SetPlayerSettings( settings )
+ player.RespawnPlayer( null )
+}
+
+void function ReviveOnClientDisconnect( entity player )
+{
+ if ( player in file.fakePlayers )
+ {
+ if ( IsValid( file.fakePlayers[ player ] ) )
+ file.fakePlayers[ player ].Destroy()
+ delete file.fakePlayers[ player ]
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_score.nut b/Northstar.CustomServers/scripts/vscripts/mp/_score.nut
new file mode 100644
index 000000000..b8ea6074b
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_score.nut
@@ -0,0 +1,133 @@
+untyped
+
+global function Score_Init
+
+global function AddPlayerScore
+global function ScoreEvent_PlayerKilled
+global function ScoreEvent_TitanDoomed
+global function ScoreEvent_TitanKilled
+global function ScoreEvent_NPCKilled
+
+global function ScoreEvent_SetEarnMeterValues
+global function ScoreEvent_SetupEarnMeterValuesForMixedModes
+
+struct {
+ bool firstStrikeDone = false
+} file
+
+void function Score_Init()
+{
+
+}
+
+void function AddPlayerScore( entity targetPlayer, string scoreEventName, entity associatedEnt = null, string noideawhatthisis = "", int pointValueOverride = -1 )
+{
+ ScoreEvent event = GetScoreEvent( scoreEventName )
+
+ if ( !event.enabled )
+ return
+
+ var associatedHandle = 0
+ if ( associatedEnt != null )
+ associatedHandle = associatedEnt.GetEncodedEHandle()
+
+ if ( pointValueOverride != -1 )
+ event.pointValue = pointValueOverride
+
+ float scale = targetPlayer.IsTitan() ? event.coreMeterScalar : 1.0
+ float earnValue = event.earnMeterEarnValue * scale
+ float ownValue = event.earnMeterOwnValue * scale
+
+ PlayerEarnMeter_AddEarnedAndOwned( targetPlayer, earnValue * scale, ownValue * scale )
+
+ Remote_CallFunction_NonReplay( targetPlayer, "ServerCallback_ScoreEvent", event.eventId, event.pointValue, event.displayType, associatedHandle, ownValue, earnValue )
+
+ if ( event.displayType & eEventDisplayType.CALLINGCARD ) // callingcardevents are shown to all players
+ {
+ foreach ( entity player in GetPlayerArray() )
+ {
+ if ( player == targetPlayer ) // targetplayer already gets this in the scorevent callback
+ continue
+
+ Remote_CallFunction_NonReplay( player, "ServerCallback_CallingCardEvent", event.eventId, associatedHandle )
+ }
+ }
+
+ if ( ScoreEvent_HasConversation( event ) )
+ thread Delayed_PlayConversationToPlayer( event.conversation, targetPlayer, event.conversationDelay )
+}
+
+void function ScoreEvent_PlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ AddPlayerScore( attacker, "KillPilot", victim )
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_HEADSHOT )
+ AddPlayerScore( attacker, "Headshot", victim )
+
+ if ( !file.firstStrikeDone )
+ {
+ file.firstStrikeDone = true
+ AddPlayerScore( attacker, "FirstStrike", attacker )
+ }
+
+ if ( victim.IsTitan() )
+ ScoreEvent_TitanKilled( victim, attacker, damageInfo )
+}
+
+void function ScoreEvent_TitanDoomed( entity titan, entity attacker, var damageInfo )
+{
+ // will this handle npc titans with no owners well? i have literally no idea
+
+ if ( titan.IsNPC() )
+ AddPlayerScore( attacker, "DoomAutoTitan", titan )
+ else
+ AddPlayerScore( attacker, "DoomTitan", titan )
+}
+
+void function ScoreEvent_TitanKilled( entity victim, entity attacker, var damageInfo )
+{
+ // will this handle npc titans with no owners well? i have literally no idea
+
+ if ( attacker.IsTitan() )
+ AddPlayerScore( attacker, "TitanKillTitan", victim.GetTitanSoul().GetOwner() )
+ else
+ AddPlayerScore( attacker, "KillTitan", victim.GetTitanSoul().GetOwner() )
+}
+
+void function ScoreEvent_NPCKilled( entity victim, entity attacker, var damageInfo )
+{
+ AddPlayerScore( attacker, ScoreEventForNPCKilled( victim, damageInfo ), victim )
+}
+
+
+
+void function ScoreEvent_SetEarnMeterValues( string eventName, float earned, float owned, float coreScale = 1.0 )
+{
+ ScoreEvent event = GetScoreEvent( eventName )
+ event.earnMeterEarnValue = earned
+ event.earnMeterOwnValue = owned
+ event.coreMeterScalar = coreScale
+}
+
+void function ScoreEvent_SetupEarnMeterValuesForMixedModes() // mixed modes in this case means modes with both pilots and titans
+{
+ // todo needs earn/overdrive values
+ // player-controlled stuff
+ ScoreEvent_SetEarnMeterValues( "KillPilot", 0.0, 0.05 )
+ ScoreEvent_SetEarnMeterValues( "KillTitan", 0.0, 0.15 )
+ ScoreEvent_SetEarnMeterValues( "TitanKillTitan", 0.0, 0.0 ) // unsure
+ ScoreEvent_SetEarnMeterValues( "PilotBatteryStolen", 0.0, 0.35 )
+
+ // ai
+ ScoreEvent_SetEarnMeterValues( "KillGrunt", 0.0, 0.02, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "KillSpectre", 0.0, 0.02, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "LeechSpectre", 0.0, 0.02 )
+ ScoreEvent_SetEarnMeterValues( "KillStalker", 0.0, 0.02, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "KillSuperSpectre", 0.0, 0.1, 0.5 )
+}
+
+void function ScoreEvent_SetupEarnMeterValuesForTitanModes()
+{
+ // todo needs earn/overdrive values
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_serverflags.nut b/Northstar.CustomServers/scripts/vscripts/mp/_serverflags.nut
new file mode 100644
index 000000000..a665463da
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_serverflags.nut
@@ -0,0 +1,35 @@
+untyped
+
+globalize_all_functions
+
+function GiveServerFlag( player, passive )
+{
+ if ( !( player.serverFlags & passive ) )
+ {
+ player.serverFlags = player.serverFlags | passive
+ }
+
+ // enter/exit functions for specific passives
+ switch ( passive )
+ {
+ }
+}
+
+function TakeServerFlag( player, passive )
+{
+ if ( !PlayerHasServerFlag( player, passive ) )
+ return
+
+ player.serverFlags = player.serverFlags & ( ~passive )
+
+ // enter/exit functions for specific passives
+ switch ( passive )
+ {
+ }
+
+}
+
+bool function PlayerHasServerFlag( player, passive )
+{
+ return bool( player.serverFlags & passive )
+}
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_sniper_spectres.nut b/Northstar.CustomServers/scripts/vscripts/mp/_sniper_spectres.nut
new file mode 100644
index 000000000..ce513259b
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_sniper_spectres.nut
@@ -0,0 +1,485 @@
+untyped
+
+const MAXNODES_PER_SNIPERSPOT = 4
+const MAX_SNIPERSPOTS = 30 // for speed of iterating through the array
+const SNIPERSPOT_RADIUSCHECK = 200
+const SNIPERSPOT_HEIGHTCHECK = 160
+const SNIPERNODE_TOOCLOSE_SQR = 2500//50x50
+
+global function SniperSpectres_Init
+global function TowerDefense_AddSniperLocation
+global function Dev_AddSniperLocation
+global function DebugDrawSniperLocations
+global function Sniper_MoveToNewLocation
+global function Sniper_FreeSniperNodeOnDeath
+global function SniperCloak
+global function SniperDeCloak
+
+
+function SniperSpectres_Init()
+{
+ FlagInit( "TD_SniperLocationsInit" )
+
+ level.TowerDefense_SniperNodes <- []
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+}
+
+void function EntitiesDidLoad()
+{
+ thread SniperLocationsInit()
+}
+
+/************************************************************************************************\
+
+######## ####### ####### ## ######
+ ## ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ######
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ## ##
+ ## ####### ####### ######## ######
+
+\************************************************************************************************/
+function TowerDefense_AddSniperLocation( origin, yaw, heightCheck = SNIPERSPOT_HEIGHTCHECK )
+{
+ Assert( !Flag( "TD_SniperLocationsInit" ), "sniper locations added too late" )
+ Assert( ( level.TowerDefense_SniperNodes.len() < MAX_SNIPERSPOTS ), "adding too many snper locations, max is " + MAX_SNIPERSPOTS )
+
+ local loc = CreateSniperLocation( origin, yaw, heightCheck )
+
+ level.TowerDefense_SniperNodes.append( loc )
+}
+
+function Dev_AddSniperLocation( origin, yaw, heightCheck = SNIPERSPOT_HEIGHTCHECK )
+{
+ thread __AddSniperLocationInternal( origin, yaw, heightCheck )
+}
+
+function __AddSniperLocationInternal( origin, yaw, heightCheck )
+{
+ local loc = CreateSniperLocation( origin, yaw, heightCheck )
+ SniperLocationSetup( loc )
+ DebugDrawSingleSniperLocation( loc, 4.0 )
+}
+
+function DebugDrawSniperLocations()
+{
+ foreach ( loc in level.TowerDefense_SniperNodes )
+ DebugDrawSingleSniperLocation( loc, 600.0 )
+}
+
+function DebugDrawSingleSniperLocation( loc, float time )
+{
+ if ( !loc.maxGuys )
+ {
+ DebugDrawSniperSpot( expect vector( loc.pos ), [ 32.0, 40.0, 48.0 ], 255, 0, 0, time, loc.yaw )
+ return
+ }
+
+ DebugDrawSniperSpot( expect vector( loc.pos ), [ 28.0 ], 20, 20, 20, time, loc.yaw )
+
+ foreach ( node in loc.coverNodes )
+ DebugDrawSniperSpot( expect vector( node.pos ), [ 16.0, 24.0, 32.0 ], 50, 50, 255, time, null, loc.pos )
+
+ foreach ( node in loc.extraNodes )
+ DebugDrawSniperSpot( expect vector( node.pos ), [ 14.0, 22.0, 30.0 ], 255, 0, 255, time, null, loc.pos )
+}
+
+function DebugDrawSniperSpot( vector pos, array<float> radii, int r, int g, int b, float time, yaw = null, pos2 = null )
+{
+ foreach ( radius in radii )
+ DebugDrawCircle( pos, Vector( 0, 0, 0 ), radius, r, g, b, true, time )
+
+ if ( yaw != null )
+ {
+ local angles = Vector( 0, yaw, 0 )
+ local forward = AnglesToForward( angles )
+ local right = AnglesToRight( angles )
+ local length = radii[ radii.len() - 1 ]
+ local endPos = pos + ( forward * ( length * 1.75 ) )
+ local rightPos = pos + ( right * length )
+ local leftPos = pos + ( right * -length )
+ DebugDrawLine( pos, endPos, r, g, b, true, time )
+ DebugDrawLine( rightPos, endPos, r, g, b, true, time )
+ DebugDrawLine( leftPos, endPos, r, g, b, true, time )
+
+ local ring = GetDesirableRing( pos, yaw )
+ DebugDrawCircle( expect vector( ring.pos ), Vector( 0, 0, 0 ), expect float( ring.radius ), r, g, b, true, time )
+ }
+
+ if ( pos2 != null )
+ DebugDrawLine( pos, pos2, r, g, b, true, time )
+}
+
+/************************************************************************************************\
+
+######## ### ######## ## ## #### ## ## ######
+## ## ## ## ## ## ## ## ### ## ## ##
+## ## ## ## ## ## ## ## #### ## ##
+######## ## ## ## ######### ## ## ## ## ## ####
+## ######### ## ## ## ## ## #### ## ##
+## ## ## ## ## ## ## ## ### ## ##
+## ## ## ## ## ## #### ## ## ######
+
+\************************************************************************************************/
+//HACK -> this should probably move into code
+function Sniper_MoveToNewLocation( entity sniper )
+{
+ sniper.EndSignal( "OnDeath" )
+ sniper.EndSignal( "OnDestroy" )
+
+ delaythread( 2 ) SniperCloak( sniper )
+
+ //go searching for nodes that are up somewhere
+ local sniperNode = GetRandomSniperNodeWithin( sniper, 3000 )
+
+ Sniper_FreeSniperNode( sniper )//free his current node
+ Sniper_TakeSniperNode( sniper, sniperNode )
+ Sniper_AssaultLocation( sniper, sniperNode )
+
+ WaitSignal( sniper, "OnFinishedAssault", "OnDeath", "OnDestroy", "AssaultTimeOut" )
+
+ SniperDeCloak( sniper )
+}
+
+function Sniper_TakeSniperNode( sniper, sniperNode )
+{
+ Assert( sniper.s.sniperNode == null ) // didn't free the last one
+ sniper.s.sniperNode = sniperNode
+
+ Assert( sniperNode.locked == false )// someone else already has it?
+ sniperNode.locked = true
+
+ local loc = sniperNode.loc
+ loc.numGuys++
+}
+
+function Sniper_FreeSniperNode( sniper )
+{
+ local sniperNode = sniper.s.sniperNode
+ if ( sniperNode == null )
+ return
+
+ sniper.s.sniperNode = null
+
+ local loc = sniperNode.loc
+ loc.numGuys--
+ sniperNode.locked = false
+}
+
+function Sniper_FreeSniperNodeOnDeath( entity sniper )
+{
+ sniper.WaitSignal( "OnDeath" )
+ Sniper_FreeSniperNode( sniper )
+}
+
+void function SniperCloak( entity sniper )
+{
+ if ( !IsAlive( sniper ) )
+ return
+
+ if ( !sniper.CanCloak() )
+ return
+
+ sniper.kv.allowshoot = 0
+ sniper.SetCloakDuration( 3.0, -1, 0 )
+ sniper.Minimap_Hide( TEAM_IMC, null )
+ sniper.Minimap_Hide( TEAM_MILITIA, null )
+}
+
+void function SniperDeCloak( entity sniper )
+{
+ if ( !IsAlive( sniper ) )
+ return
+
+ sniper.kv.allowshoot = 1
+ sniper.SetCloakDuration( 0, 0, 1.5 )
+ sniper.Minimap_AlwaysShow( TEAM_IMC, null )
+ sniper.Minimap_AlwaysShow( TEAM_MILITIA, null )
+}
+
+function Sniper_AssaultLocation( sniper, sniperNode )
+{
+ Assert( sniper.s.sniperNode == sniperNode ) // didn't get the right one
+
+ local origin = sniperNode.pos
+ local loc = sniperNode.loc
+ local angles = Vector( 0, loc.yaw, 0 )
+
+ Assert( "assaultPoint" in sniper.s )
+ sniper.AssaultPoint( origin )
+ sniper.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+}
+
+function GetRandomSniperNodeWithin( sniper, maxDist )
+{
+ Assert( level.TowerDefense_SniperNodes.len() >= 2 )
+
+ local origin = sniper.GetOrigin()
+ local locations = SniperNodeWithin( level.TowerDefense_SniperNodes, origin, maxDist )
+
+ if ( locations.len() )
+ locations.randomize()
+
+ local goalNode = FindFreeSniperNode( locations )
+ if ( goalNode != null )
+ return goalNode
+
+ //if we get here it's because there are no free nodes within the maxDist
+ locations = SniperNodeClosest( level.TowerDefense_SniperNodes, origin )
+
+ goalNode = FindFreeSniperNode( locations )
+ Assert ( goalNode != null )
+
+ return goalNode
+}
+
+function FindFreeSniperNode( locations )
+{
+ foreach( loc in locations )
+ {
+ //is if filled up?
+ if ( loc.numGuys >= loc.maxGuys )
+ continue
+
+ //grab the first unlocked cover node
+ foreach ( node in loc.coverNodes )
+ {
+ if ( node.locked )
+ continue
+
+ return node
+ }
+
+ //ok then grab the first unlocked extra node
+ foreach ( node in loc.extraNodes )
+ {
+ if ( node.locked )
+ continue
+
+ return node
+ }
+ }
+
+ return null
+}
+
+//ArrayWithin() copy
+function SniperNodeWithin( Array, origin, maxDist )
+{
+ maxDist *= maxDist
+
+ local resultArray = []
+ foreach( loc in Array )
+ {
+ local testspot = null
+ testspot = loc.pos
+
+ local dist = DistanceSqr( origin, testspot )
+ if ( dist <= maxDist )
+ resultArray.append( loc )
+ }
+ return resultArray
+}
+
+//ArrayClosest() copy
+function SniperNodeClosest( Array, origin )
+{
+ Assert( type( Array ) == "array" )
+ local allResults = SniperArrayDistanceResults( Array, origin )
+
+ allResults.sort( SniperArrayDistanceCompare )
+
+ local returnEntities = []
+
+ foreach ( index, result in allResults )
+ {
+ returnEntities.insert( index, result.loc )
+ }
+
+ // the actual distances aren't returned
+ return returnEntities
+}
+
+function SniperArrayDistanceResults( Array, origin )
+{
+ Assert( type( Array ) == "array" )
+ local allResults = []
+
+ foreach ( loc in Array )
+ {
+ local results = {}
+ local testspot = null
+
+ testspot = loc.pos
+
+ results.distanceSqr <- LengthSqr( testspot - origin )
+ results.loc <- loc
+ allResults.append( results )
+ }
+
+ return allResults
+}
+
+function SniperArrayDistanceCompare( a, b )
+{
+ if ( a.distanceSqr > b.distanceSqr )
+ return 1
+ else if ( a.distanceSqr < b.distanceSqr )
+ return -1
+
+ return 0;
+}
+
+
+
+/************************************************************************************************\
+
+######## ######## ######## ###### ### ## ######
+## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ##
+######## ######## ###### ####### ## ## ## ## ##
+## ## ## ## ## ######### ## ##
+## ## ## ## ## ## ## ## ## ## ##
+## ## ## ######## ###### ## ## ######## ######
+
+\************************************************************************************************/
+function CreateSniperLocation( origin, yaw, heightCheck )
+{
+ local loc = {}
+ loc.pos <- origin
+ loc.yaw <- yaw
+ loc.heightCheck <- heightCheck
+ loc.numGuys <- 0
+ loc.maxGuys <- 0
+ loc.coverNodes <- []
+ loc.extraNodes <- []
+
+ return loc
+}
+
+function CreateSniperNode( location, origin )
+{
+ local node = {}
+ node.locked <- false
+ node.loc <- location
+ node.pos <- origin
+
+ return node
+}
+
+function SniperLocationsInit()
+{
+ FlagSet( "TD_SniperLocationsInit" )
+ local time = Time()
+
+ foreach ( loc in level.TowerDefense_SniperNodes )
+ {
+ SniperLocationSetup( loc )
+ wait 0.1 //space out all the slow stuff so it doesn't happen on the same frame
+ }
+
+ printt( "<<<<<***********************************************************>>>>>" )
+ printt( "SniperLocationsInit() took ", Time() - time, " seconds to complete" )
+ printt( "<<<<<***********************************************************>>>>>" )
+}
+
+function SniperLocationSetup( loc )
+{
+ array<vector> extraPos = GetNeighborPositionsAroundSniperLocation( expect vector( loc.pos ), expect float( loc.yaw ), expect float( loc.heightCheck ), MAXNODES_PER_SNIPERSPOT )
+ foreach ( origin in extraPos )
+ {
+ local node = CreateSniperNode( loc, origin )
+ loc.extraNodes.append( node )
+ }
+
+ loc.maxGuys = loc.coverNodes.len() + loc.extraNodes.len()
+ if ( loc.maxGuys == 0 )
+ printt( "sniper spot at [ " + loc.pos + " ] has no nodes around it within " + SNIPERSPOT_RADIUSCHECK + " units." )
+ Assert( loc.maxGuys <= MAXNODES_PER_SNIPERSPOT )
+}
+
+array<vector> function GetNeighborPositionsAroundSniperLocation( vector pos, float yaw, float heightCheck, int max )
+{
+ local height = pos.z
+ local isSpectre = true
+ local radius = SNIPERSPOT_RADIUSCHECK
+ array<vector> goalPos = []
+
+ array<vector> neighborPos = NavMesh_GetNeighborPositions( pos, HULL_HUMAN, MAXNODES_PER_SNIPERSPOT )
+ neighborPos = SortPositionsByClosestToPos( neighborPos, pos, yaw )
+ foreach ( origin in neighborPos )
+ {
+ if ( fabs( origin.z - height ) > heightCheck )
+ continue
+
+ if ( !IsMostDesireablePos( origin, pos, yaw ) )
+ continue
+
+ if ( IsPosTooCloseToOtherPositions( origin, goalPos ) )
+ continue
+
+ goalPos.append( origin )
+ if ( goalPos.len() == max )
+ break
+ }
+
+ return goalPos
+}
+
+array<vector> function SortPositionsByClosestToPos( array<vector> neighborPos, vector pos, float yaw )
+{
+ table ring = GetDesirableRing( pos, yaw )
+ vector testPos = expect vector( ring.pos )
+
+ array<vector> returnOrigins = ArrayClosestVector( neighborPos, testPos )
+ return returnOrigins
+}
+
+bool function IsPosTooCloseToOtherPositions( vector testPos, array<vector> positions )
+{
+ foreach ( pos in positions )
+ {
+ if ( DistanceSqr( pos, testPos ) <= SNIPERNODE_TOOCLOSE_SQR )
+ return true
+ }
+ return false
+}
+
+function IsMostDesireablePos( testPos, sniperPos, yaw )
+{
+ /*
+ what this function does is actually draw a circle out infront of the position based on the yaw.
+ then it checks to see if the node is within that circle.
+ Since most sniper positions are on EDGES of buildings, windows, etc, this techinique helps grab more nodes along the edge
+ */
+
+ table ring = GetDesirableRing( sniperPos, yaw )
+ local radiusSqr = ring.radius * ring.radius
+
+ if ( Distance2DSqr( testPos, ring.pos ) <= radiusSqr )
+ return true
+
+ return false
+}
+
+table function GetDesirableRing( pos, yaw )
+{
+ local dist = 200
+ local radius = 300
+
+ local vec = AnglesToForward( Vector( 0, yaw, 0 ) ) * dist
+ local testPos = pos + vec
+
+ table ring = {}
+ ring.pos <- testPos
+ ring.radius <- radius
+ return ring
+}
+
+
+
+
+
+
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_spawn_functions.nut b/Northstar.CustomServers/scripts/vscripts/mp/_spawn_functions.nut
new file mode 100644
index 000000000..3d9b84f3a
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_spawn_functions.nut
@@ -0,0 +1,60 @@
+untyped
+
+global function SpawnFunctions_Init
+
+function SpawnFunctions_Init()
+{
+ if ( IsLobby() )
+ return
+
+ // shared OnSpawned callbacks
+ AddSpawnCallback( "script_mover", SpawnScriptMover )
+ AddSpawnCallback( "path_track", SpawnPathTrack )
+ AddSpawnCallback( "info_hint", SpawnInfoHint )
+ AddDeathCallback( "npc_titan", EmptyDeathCallback ) // so death info gets sent to client
+
+ // Arc Cannon Targets
+ foreach ( classname, val in ArcCannonTargetClassnames )
+ {
+ AddSpawnCallback( classname, AddToArcCannonTargets )
+ }
+
+ foreach ( classname, val in ProximityTargetClassnames )
+ {
+ AddSpawnCallback( classname, AddToProximityTargets )
+ }
+}
+
+void function EmptyDeathCallback( entity _1, var _2 )
+{
+}
+
+
+void function SpawnPathTrack( entity node )
+{
+ if ( node.HasKey( "WaitSignal" ) )
+ RegisterSignal( node.kv.WaitSignal )
+
+ if ( node.HasKey( "SendSignal" ) )
+ RegisterSignal( node.kv.SendSignal )
+
+ if ( node.HasKey( "WaitFlag" ) )
+ FlagInit( expect string( node.kv.WaitFlag ) )
+
+ if ( node.HasKey( "SetFlag" ) )
+ FlagInit( expect string( node.kv.SetFlag ) )
+}
+
+void function SpawnScriptMover( entity ent )
+{
+ if ( ent.HasKey( "custom_health" ) )
+ {
+ //printt( "setting health on " + ent + " to " + ent.kv.custom_health.tointeger() )
+ ent.SetHealth( ent.kv.custom_health.tointeger() )
+ }
+}
+
+void function SpawnInfoHint( entity ent )
+{
+ Assert( !ent.HasKey( "hotspot" ) || ent.kv.hotspot.tolower() in level.hotspotHints, "info_hint at " + ent.GetOrigin() + " has unknown hotspot hint: " + ent.kv.hotspot.tolower() )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_spectre_rack.nut b/Northstar.CustomServers/scripts/vscripts/mp/_spectre_rack.nut
new file mode 100644
index 000000000..a76c0fc9c
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_spectre_rack.nut
@@ -0,0 +1,395 @@
+
+global function SpectreRack_Init
+global function IsStalkerRack
+global function SpawnFromStalkerRack
+global function AddSpectreRackCallback
+global function GetSpectreRackFromEnt
+global function SetupSpectreRack
+global function SpectreRackActivationEffects
+global function TrackFriendlySpectre
+
+const FX_GREEN_GLOW = $"P_spectre_rack_glow_idle"
+const WARNING_LIGHT_BLINK = $"warning_light_orange_blink"
+const SPECTRE_RACK_ACHIEVEMENT_COUNT = 6
+
+global struct SpectreRackSpectre
+{
+ string attachName
+ entity dummyModel
+ entity glowFX
+ entity spawner
+}
+
+global struct SpectreRack
+{
+ entity rackEnt
+ array<SpectreRackSpectre> spectreRackSpectres
+}
+
+struct
+{
+ int playersSpectreArrayIdx
+ array<string> spectreRackTypes
+ array<SpectreRack> spectreRacks
+ array<void functionref( entity, entity )> callbackFuncs
+} file
+
+void function AddSpectreRackCallback( void functionref( entity, entity ) func )
+{
+ Assert( !file.callbackFuncs.contains( func ) )
+ file.callbackFuncs.append( func )
+}
+
+void function SpectreRack_Init()
+{
+ if ( reloadingScripts )
+ return
+
+ file.spectreRackTypes.append( "npc_spectre_rack_wall" )
+ file.spectreRackTypes.append( "npc_spectre_rack_multi" )
+ file.spectreRackTypes.append( "npc_spectre_rack_triple" )
+ //file.spectreRackTypes.append( "npc_spectre_rack_portable" )
+ //file.spectreRackTypes.append( "npc_spectre_rack_palette" )
+
+ PrecacheParticleSystem( FX_GREEN_GLOW )
+ PrecacheParticleSystem( WARNING_LIGHT_BLINK )
+
+ foreach ( string rackType in file.spectreRackTypes )
+ {
+ AddSpawnCallbackEditorClass( "prop_dynamic", rackType, SetupSpectreRack )
+ }
+
+ if ( IsSingleplayer() )
+ {
+ file.playersSpectreArrayIdx = CreateScriptManagedEntArray()
+ AddSpectreRackCallback( TrySpectreAchievement )
+ }
+}
+
+bool function IsStalkerRack( entity ent )
+{
+ if ( !ent.HasKey( "editorclass" ) )
+ return false
+ string editorclass = ent.GetValueForKey( "editorclass" )
+ return file.spectreRackTypes.contains( editorclass )
+}
+
+void function SetupSpectreRack( entity rack )
+{
+ SpectreRack spectreRack
+ spectreRack.rackEnt = rack
+
+ // Get attach point info from the model being used
+ while( true )
+ {
+ int attachIndex = spectreRack.spectreRackSpectres.len() + 1
+ string attachment = "spectre_attach_" + attachIndex
+
+ int id = rack.LookupAttachment( attachment )
+ if ( id == 0 )
+ break
+
+ SpectreRackSpectre spectreRackSpectre
+ spectreRackSpectre.attachName = attachment
+ spectreRack.spectreRackSpectres.append( spectreRackSpectre )
+ }
+
+ // Get linked spawner
+ array<entity> linkedEnts = rack.GetLinkEntArray()
+ int spawnerCount = 0
+ foreach ( index, ent in linkedEnts )
+ {
+ if ( IsSpawner( ent ) )
+ {
+ spectreRack.spectreRackSpectres[index].spawner = ent
+ spawnerCount++
+ }
+ }
+ Assert( spawnerCount == spectreRack.spectreRackSpectres.len(), "Spectre rack " + rack + " at: " + rack.GetOrigin() + " " + rack.GetValueForKey( "editorclass" ) + " must link to exactly " + spectreRack.spectreRackSpectres.len() + " spawner" )
+
+ // Create dummy spectre models to idle on the rack
+ foreach ( spectreRackSpectre in spectreRack.spectreRackSpectres )
+ {
+ int attachID = rack.LookupAttachment( spectreRackSpectre.attachName )
+ vector origin = rack.GetAttachmentOrigin( attachID )
+ vector angles = rack.GetAttachmentAngles( attachID )
+
+ var spawnerKeyValues = spectreRackSpectre.spawner.GetSpawnEntityKeyValues()
+ expect table( spawnerKeyValues )
+ asset model = spectreRackSpectre.spawner.GetSpawnerModelName()
+ int skin
+ if ( "skin" in spawnerKeyValues )
+ {
+ skin = int( spawnerKeyValues.skin )
+ }
+
+ string idleAnim = GetIdleAnimForSpawner( spectreRackSpectre.spawner )
+ entity dummySpectre = CreatePropDynamic( model, origin, angles )
+ dummySpectre.SetSkin( skin )
+ dummySpectre.SetParent( rack, spectreRackSpectre.attachName )
+ thread PlayAnimTeleport( dummySpectre, idleAnim, rack, spectreRackSpectre.attachName )
+
+ spectreRackSpectre.dummyModel = dummySpectre
+ }
+
+ // Create effects on the rack
+ if ( !rack.HasKey( "DisableStatusLights" ) || rack.GetValueForKey( "DisableStatusLights" ) == "0" )
+ SpectreRackCreateFx( spectreRack, FX_GREEN_GLOW )
+
+ file.spectreRacks.append( spectreRack )
+}
+
+void function SpawnFromStalkerRack( entity rack, entity activator = null )
+{
+ Assert( IsNewThread(), "Must be threaded off." )
+ SpectreRack spectreRack = GetSpectreRackFromEnt( rack )
+ Assert( IsValid( spectreRack.rackEnt ) )
+
+ thread SpectreRackActivationEffects( spectreRack )
+ thread SpectreRackActivationSpawners( spectreRack, activator )
+
+ if ( IsValid( activator ) && activator.IsPlayer() )
+ UnlockAchievement( activator, achievements.HACK_STALKERS )
+}
+
+void function SpectreRackActivationEffects( SpectreRack spectreRack )
+{
+ EndSignal( spectreRack, "OnDestroy" )
+ EndSignal( spectreRack.rackEnt, "OnDestroy" )
+
+ OnThreadEnd
+ (
+ function() : ( spectreRack )
+ {
+ if ( IsValid( spectreRack ) )
+ SpectreRackDestroyFx( spectreRack )
+ }
+ )
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, spectreRack.rackEnt.GetOrigin() + Vector( 0, 0, 72), "colony_spectre_initialize_beep" )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, spectreRack.rackEnt.GetOrigin() + Vector( 0, 0, 72), "corporate_spectrerack_activate" )
+
+ SpectreRackDestroyFx( spectreRack )
+ SpectreRackCreateFx( spectreRack, WARNING_LIGHT_BLINK )
+
+ // Let the flash FX linger a bit longer, then the thread end will kill the fx
+ wait 6
+}
+
+void function SpectreRackActivationSpawners( SpectreRack spectreRack, entity activator = null )
+{
+ EndSignal( spectreRack, "OnDestroy" )
+ EndSignal( spectreRack.rackEnt, "OnDestroy" )
+
+ array<int> spawnOrder
+ for ( int i = 0 ; i < spectreRack.spectreRackSpectres.len() ; i++ )
+ {
+ spawnOrder.append(i)
+ }
+
+ spawnOrder.randomize()
+
+ foreach ( int index in spawnOrder )
+ {
+ thread SpectreRackReleaseSpectre( spectreRack, index, activator )
+
+ wait RandomFloatRange( 0.0, 0.25 )
+ }
+}
+
+void function SpectreRackReleaseSpectre( SpectreRack spectreRack, int index, entity activator = null )
+{
+ SpectreRackSpectre spectreRackSpectre = spectreRack.spectreRackSpectres[ index ]
+ if ( !IsValid( spectreRackSpectre.dummyModel ) )
+ return
+
+ entity rackEnt = spectreRack.rackEnt
+
+ entity dummy = spectreRackSpectre.dummyModel
+ Assert( IsValid ( dummy ) )
+
+ entity spawner = spectreRackSpectre.spawner
+ Assert( IsValid ( spawner ) )
+
+ EndSignal( spectreRackSpectre, "OnDestroy" )
+ EndSignal( rackEnt, "OnDestroy" )
+
+ var spawnerKeyValues = spawner.GetSpawnEntityKeyValues()
+ expect table( spawnerKeyValues )
+ if ( "script_delay" in spawnerKeyValues )
+ {
+ float delay = float( spawnerKeyValues.script_delay )
+ wait delay
+ }
+
+ if ( IsValid( dummy ) )
+ dummy.Destroy()
+ entity spectre = spawner.SpawnEntity()
+ DispatchSpawn( spectre )
+ spectre.ContextAction_SetBusy()
+
+ if ( IsValid( activator ) )
+ {
+ SetTeam( spectre, activator.GetTeam() )
+ //spectre.DisableBehavior( "Assault" )
+ /*
+ if ( activator.IsPlayer() )
+ {
+ NPCFollowsPlayer( spectre, activator )
+ }
+ else if ( activator.IsNPC() )
+ {
+ NPCFollowsNPC( spectre, activator )
+ }
+ */
+ }
+
+ string deployAnim = GetDeployAnimForSpawner( spectreRackSpectre.spawner )
+ string idleAnim = GetIdleAnimForSpawner( spectreRackSpectre.spawner )
+
+ EndSignal( spectre, "OnDeath" )
+
+ string attachment = spectreRackSpectre.attachName
+ spectre.SetParent( rackEnt, attachment )
+ thread PlayAnimTeleport( spectre, idleAnim, rackEnt, attachment )
+
+ if ( CoinFlip() )
+ EmitSoundOnEntity( spectre, "diag_stalker_generic" )
+
+ spectre.SetNoTarget( true )
+ waitthread PlayAnim( spectre, deployAnim, rackEnt, attachment )
+ spectre.ClearParent()
+ float yaw = spectre.GetAngles().y
+ spectre.SetAngles( <0,yaw,0> )//spectres released on moving platforms angle correctly
+
+ foreach ( func in file.callbackFuncs )
+ {
+ thread func( spectre, activator )
+ }
+
+ spectre.SetTitle( spectre.GetSettingTitle() )
+ Highlight_SetFriendlyHighlight( spectre, "sp_friendly_pilot" )
+ ShowName( spectre )
+ wait 1
+ spectre.SetNoTarget( false )
+ spectre.ContextAction_ClearBusy()
+}
+
+void function SpectreRackCreateFx( SpectreRack spectreRack, asset fxName )
+{
+ for ( int i = 0 ; i < spectreRack.spectreRackSpectres.len() ; i++ )
+ {
+ string attachment = "glow_" + i
+ int id = spectreRack.rackEnt.LookupAttachment( attachment )
+ Assert( id != 0, "Missing attachment \"" + attachment + "\" in model " + spectreRack.rackEnt.GetModelName() )
+
+ entity fx = PlayLoopFXOnEntity( fxName, spectreRack.rackEnt, attachment )
+ Assert( !IsValid( spectreRack.spectreRackSpectres[i].glowFX ) )
+ spectreRack.spectreRackSpectres[i].glowFX = fx
+ }
+}
+
+void function SpectreRackDestroyFx( SpectreRack spectreRack )
+{
+ foreach ( spectreRackSpectre in spectreRack.spectreRackSpectres )
+ {
+ entity fx = spectreRackSpectre.glowFX
+ if ( !IsValid_ThisFrame( fx ) )
+ continue
+ fx.ClearParent()
+ fx.Destroy()
+ }
+}
+
+SpectreRack function GetSpectreRackFromEnt( entity rack )
+{
+ // Get the spectre rack struct from the placed entity
+ foreach ( SpectreRack rackStruct in file.spectreRacks )
+ {
+ if ( rackStruct.rackEnt == rack )
+ return rackStruct
+ }
+ SpectreRack rackStruct
+ return rackStruct
+}
+
+string function GetIdleAnimForSpawner( entity spawner )
+{
+ string idleAnim
+ string spawnClassName = GetEditorClass( spawner )
+ if ( spawnClassName == "" )
+ spawnClassName = spawner.GetSpawnEntityClassName()
+
+ switch( spawnClassName )
+ {
+ case "npc_stalker":
+ case "npc_stalker_zombie":
+ case "npc_stalker_zombie_mossy":
+ idleAnim = "st_medbay_idle_armed"
+ break
+ case "npc_spectre":
+ idleAnim = "sp_med_bay_dropidle_A"
+ break
+ default:
+ idleAnim = "st_medbay_idle_armed"
+ break
+ }
+
+ return idleAnim
+}
+
+string function GetDeployAnimForSpawner( entity spawner )
+{
+ string deployAnim
+ string spawnClassName = GetEditorClass( spawner )
+ if ( spawnClassName == "" )
+ spawnClassName = spawner.GetSpawnEntityClassName()
+
+ switch( spawnClassName )
+ {
+ case "npc_stalker":
+ case "npc_stalker_zombie":
+ case "npc_stalker_zombie_mossy":
+ deployAnim = "st_medbay_drop_armed"
+ break
+ case "npc_spectre_suicide":
+ deployAnim = "sp_med_bay_drop_unarmed"
+ break
+ case "npc_spectre":
+ deployAnim = "sp_med_bay_drop_A"
+ break
+ default:
+ deployAnim = "st_medbay_drop_armed"
+ break
+ }
+
+ return deployAnim
+}
+
+void function TrySpectreAchievement( entity npc, entity activator )
+{
+ if ( !IsValid( activator ) )
+ return
+
+ if ( !activator.IsPlayer() )
+ return
+
+ TrackFriendlySpectre( npc, activator )
+}
+
+void function TrackFriendlySpectre( entity npc, entity player )
+{
+ if ( player.GetTeam() != npc.GetTeam() )
+ return
+
+ if ( IsStalker( npc ) )
+ AddToScriptManagedEntArray( file.playersSpectreArrayIdx, npc )
+ else
+ return
+
+ printt( "Achievment tracking stalker: " + GetScriptManagedEntArrayLen( file.playersSpectreArrayIdx ) )
+ if ( GetScriptManagedEntArrayLen( file.playersSpectreArrayIdx ) >= SPECTRE_RACK_ACHIEVEMENT_COUNT )
+ {
+ UnlockAchievement( player, achievements.HACK_ROBOTS )
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_stats.nut b/Northstar.CustomServers/scripts/vscripts/mp/_stats.nut
new file mode 100644
index 000000000..0e8b58f45
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_stats.nut
@@ -0,0 +1,78 @@
+global function Stats_Init
+global function AddStatCallback
+global function Stats_SaveStatDelayed
+global function PlayerStat_GetCurrentInt
+global function PlayerStat_GetCurrentFloat
+global function UpdatePlayerStat
+global function IncrementPlayerDidPilotExecutionWhileCloaked
+global function UpdateTitanDamageStat
+global function UpdateTitanWeaponDamageStat
+global function UpdateTitanCoreEarnedStat
+global function PreScoreEventUpdateStats
+global function PostScoreEventUpdateStats
+global function Stats_OnPlayerDidDamage
+
+void function Stats_Init()
+{
+
+}
+
+void function AddStatCallback(string statCategory, string statAlias, string statSubAlias, void functionref(entity, float, string) callback, string subRef)
+{
+
+}
+
+void function Stats_SaveStatDelayed(entity player, string statCategory, string statAlias, string statSubAlias)
+{
+
+}
+
+int function PlayerStat_GetCurrentInt(entity player, string statCategory, string statAlias, string statSubAlias)
+{
+ return 0
+}
+
+float function PlayerStat_GetCurrentFloat(entity player, string statCategory, string statAlias, string statSubAlias)
+{
+ return 0
+}
+
+void function UpdatePlayerStat(entity player, string statCategory, string subStat, int count = 0)
+{
+
+}
+
+void function IncrementPlayerDidPilotExecutionWhileCloaked(entity player)
+{
+
+}
+
+void function UpdateTitanDamageStat(entity attacker, float savedDamage, var damageInfo)
+{
+
+}
+
+void function UpdateTitanWeaponDamageStat(entity attacker, float savedDamage, var damageInfo)
+{
+
+}
+
+void function UpdateTitanCoreEarnedStat( entity player, entity titan )
+{
+
+}
+
+void function PreScoreEventUpdateStats(entity attacker, entity ent)
+{
+
+}
+
+void function PostScoreEventUpdateStats(entity attacker, entity ent)
+{
+
+}
+
+void function Stats_OnPlayerDidDamage(entity player, var damageInfo)
+{
+
+}
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_titan_npc.nut b/Northstar.CustomServers/scripts/vscripts/mp/_titan_npc.nut
new file mode 100644
index 000000000..582850875
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_titan_npc.nut
@@ -0,0 +1,818 @@
+untyped
+
+global function TitanNPC_Init
+
+global function CodeCallback_PlayerRequestClimbInNPCTitan
+global function ResetTitanLoadoutFromPrimary
+
+global function NPCTitanNextMode
+global function NPCTitanInitModeOnPlayerRespawn
+global function SetupAutoTitan
+global function SetupNPC_TitanTitle
+global function SetPlayerPetTitan
+global function AutoTitanChangedEnemy
+global function PlayAutoTitanConversation
+global function CreateTitanModelAndSkinSetup
+global function SetWeaponCooldowns
+
+global function ResetTitanBuildTime
+
+global function CreateNPCTitanFromSettings
+
+global function FreeAutoTitan
+
+global function GetRandomTitanWeapon
+
+global function SpawnTitanBatteryOnDeath
+global function CreateTitanBattery
+
+global function WaitForHotdropToEnd
+global function ResetCoreKillCounter
+
+const TITAN_USE_HOLD_PROMPT = "Hold [USE] to Pilot||Hold [USE] to Rodeo"
+const TITAN_USE_PRESS_PROMPT = "Press [USE] to Pilot||Press [USE] to Rodeo"
+
+const int BATTERY_DROP_BOSS = 4
+
+const float BATTERY_DROP_HEALTH_FRAC_SURE = 0.2
+const float BATTERY_DROP_HEALTH_FRAC_MID = 0.5
+
+const int BATTERY_DROP_MID_CHANCE = 70
+const int BATTERY_DROP_LOW_CHANCE = 40
+
+struct
+{
+ int coreKillCounter = 0
+} file
+
+function TitanNPC_Init()
+{
+ RegisterSignal( "ChangedTitanMode" )
+ RegisterSignal( "PROTO_WeaponPickup" )
+
+ AddSoulDeathCallback( AutoTitanDestroyedCheck )
+
+ #if R1_VGUI_MINIMAP
+ Minimap_PrecacheMaterial( $"vgui/HUD/threathud_titan_friendlyself" )
+ Minimap_PrecacheMaterial( $"vgui/HUD/threathud_titan_friendlyself_guard" )
+ #endif
+
+ if ( IsSingleplayer() )
+ {
+ AddSpawnCallbackEditorClass( "script_ref", "script_titan_battery", SpawnTitanBattery )
+ AddDeathCallback( "npc_titan", SpawnTitanBatteryOnDeath )
+ AddDeathCallback( "npc_titan", TitanAchievementTracking_SP )
+ }
+}
+
+void function AutoTitanDestroyedCheck( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+ if ( !IsValid( titan ) )
+ return
+
+ entity player = soul.GetBossPlayer()
+ if ( !IsValid( player ) )
+ return
+
+ SetActiveTitanLoadoutIndex( player, -1 )
+
+ if ( player.GetPetTitan() == titan )
+ player.SetPetTitan( null )
+
+ if ( soul.IsEjecting() )
+ return
+
+ // has another titan?
+ if ( GetPlayerTitanInMap( player ) )
+ return
+
+ switch ( Riff_TitanAvailability() )
+ {
+ case eTitanAvailability.Default:
+ break
+
+ default:
+ if ( !Riff_IsTitanAvailable( player ) )
+ return
+ }
+
+ if ( GAMETYPE == SST )
+ return
+
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == eDamageSourceId.round_end )
+ return
+
+ thread PlayConversationToPlayer( "AutoTitanDestroyed", player )
+}
+
+
+
+//////////////////////////////////////////////////////////
+function SetupNPC_TitanTitle( npcTitan, player )
+{
+ npcTitan.SetBossPlayer( player )
+
+ #if R1_VGUI_MINIMAP
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ npcTitan.Minimap_SetBossPlayerMaterial( $"vgui/HUD/threathud_titan_friendlyself" )
+ break;
+
+ //case eNPCTitanMode.ROAM:
+ // break;
+
+ case eNPCTitanMode.STAY:
+ npcTitan.Minimap_SetBossPlayerMaterial( $"vgui/HUD/threathud_titan_friendlyself_guard" )
+ break;
+ }
+ #endif
+}
+
+//////////////////////////////////////////////////////////
+void function NPCTitanNextMode( entity npcTitan, entity player )
+{
+ entity soul = npcTitan.GetTitanSoul()
+ if ( !SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) && PROTO_AutoTitansDisabled() )
+ return
+
+ NPCTitanDisableCurrentMode( npcTitan, player )
+
+ local mode = player.GetPetTitanMode() + 1
+ if ( mode == eNPCTitanMode.MODE_COUNT )
+ mode = eNPCTitanMode.FOLLOW
+
+ player.SetPetTitanMode( mode )
+ npcTitan.Signal( "ChangedTitanMode" )
+
+ SetupNPC_TitanTitle( npcTitan, player )
+ NPCTitanEnableCurrentMode( npcTitan, player )
+}
+
+//////////////////////////////////////////////////////////
+function NPCTitanSetBehaviorForMode( entity npcTitan, entity player )
+{
+ entity soul = npcTitan.GetTitanSoul()
+ if ( soul == null)
+ soul = player.GetTitanSoul()
+
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ if ( soul && SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ npcTitan.SetBehaviorSelector( "behavior_mp_auto_titan_enhanced" )
+ else
+ npcTitan.SetBehaviorSelector( "behavior_mp_auto_titan" )
+ break;
+
+ //case eNPCTitanMode.ROAM:
+ // break;
+
+ case eNPCTitanMode.STAY:
+ if ( soul && SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ npcTitan.SetBehaviorSelector( "behavior_mp_auto_titan_enhanced_guard" )
+ else
+ npcTitan.SetBehaviorSelector( "behavior_mp_auto_titan_guard" )
+ break;
+ }
+}
+
+//////////////////////////////////////////////////////////
+function NPCTitanDisableCurrentMode( entity npcTitan, entity player )
+{
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ npcTitan.DisableBehavior( "Follow" )
+ break;
+
+ //case eNPCTitanMode.ROAM:
+ // break;
+
+ case eNPCTitanMode.STAY:
+ npcTitan.DisableBehavior( "Assault" )
+ break;
+ }
+}
+
+
+//////////////////////////////////////////////////////////
+function NPCTitanEnableCurrentMode( entity npcTitan, entity player )
+{
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ NPCTitanFollowPilotInit( npcTitan, player )
+ break;
+
+ //case eNPCTitanMode.ROAM:
+ // break;
+
+ case eNPCTitanMode.STAY:
+ {
+
+ local traceStart = player.EyePosition()
+ local forward = AnglesToForward( player.EyeAngles() )
+ local traceEnd = traceStart + ( forward * 12000 )
+
+ TraceResults result = TraceLine( traceStart, traceEnd, player, TRACE_MASK_BLOCKLOS, TRACE_COLLISION_GROUP_NONE )
+
+ local dir = result.endPos - npcTitan.EyePosition()
+
+ // DebugDrawLine( result.endPos, npcTitan.EyePosition(), 255, 0, 0, true, 5 )
+
+ local titanAngles;
+ if ( LengthSqr( dir ) > 100 )
+ titanAngles = VectorToAngles( dir )
+ else
+ titanAngles = player.GetAngles()
+
+ titanAngles.z = 0;
+
+ npcTitan.AssaultPointClamped( npcTitan.GetOrigin() )
+ npcTitan.AssaultSetAngles( titanAngles, true )
+ break;
+ }
+ }
+
+ NPCTitanSetBehaviorForMode( npcTitan, player )
+}
+
+
+void function AutoTitanChangedEnemy( entity titan )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ entity enemy = titan.GetEnemy()
+
+ if ( !IsAlive( enemy ) )
+ return
+
+ if ( !titan.CanSee( enemy ) )
+ return
+
+ string aliasSuffix
+ if ( enemy.IsTitan() )
+ aliasSuffix = "autoEngageTitan"
+ else if ( IsGrunt( enemy ) )
+ aliasSuffix = "autoEngageGrunt"
+ else if ( enemy.IsHuman() && enemy.IsPlayer() )
+ aliasSuffix = "autoEngagePilot"
+
+ if ( aliasSuffix == "" )
+ return
+
+ PlayAutoTitanConversation( titan, aliasSuffix )
+}
+
+function AutoTitanShouldSpeak( entity titan, entity owner, aliasSuffix )
+{
+ if ( IsForcedDialogueOnly( owner ) )
+ return false
+
+ if ( "disableAutoTitanConversation" in titan.s )
+ {
+ return false
+ }
+ //Shut Auto Titans up when game isn't active anymore
+ if ( GetGameState() >= eGameState.Postmatch )
+ {
+ return false
+ }
+
+ entity owner
+
+ if ( titan.IsPlayer() )
+ {
+ owner = titan
+ }
+ else
+ {
+ owner = GetPetTitanOwner( titan )
+ if ( !IsValid( owner ) )
+ return
+ }
+
+ if ( owner.s.autoTitanLastEngageCallout == aliasSuffix )
+ {
+ // just did this line, so significant time has to pass before we will use it again
+ return Time() > owner.s.autoTitanLastEngageCalloutTime + 28
+ }
+
+ // this is a new line, so just make sure we haven't spoken too recently
+ return Time() > owner.s.autoTitanLastEngageCalloutTime + 7
+}
+
+void function PlayAutoTitanConversation( entity titan, string aliasSuffix )
+{
+ entity owner
+
+ if ( titan.IsPlayer() )
+ {
+ owner = titan
+ }
+ else
+ {
+ owner = GetPetTitanOwner( titan )
+ if ( !IsValid( owner ) )
+ return
+ }
+
+ if ( !AutoTitanShouldSpeak( titan, owner, aliasSuffix ) ) //Only use the suffix since that's the distinguishing part of the alias, i.e. "engage_titans"
+ return
+
+ owner.s.autoTitanLastEngageCalloutTime = Time()
+ owner.s.autoTitanLastEngageCallout = aliasSuffix //Only use the suffix since that's the distinguishing part of the alias, i.e. "engage_titans"
+
+ int conversationID = GetConversationIndex( aliasSuffix )
+ Remote_CallFunction_Replay( owner, "ServerCallback_PlayTitanConversation", conversationID )
+}
+
+
+void function FreeAutoTitan( entity npcTitan )
+{
+ //npcTitan.SetEnemyChangeCallback( "" )
+
+ local bossPlayer = npcTitan.GetBossPlayer()
+
+ if ( !IsValid( bossPlayer ) )
+ return
+
+ bossPlayer.SetPetTitan( null )
+
+ local soul = npcTitan.GetTitanSoul()
+
+ npcTitan.ClearBossPlayer()
+ soul.ClearBossPlayer()
+
+ npcTitan.SetTitle( "" )
+
+ npcTitan.Signal( "TitanStopsThinking" )
+ npcTitan.UnsetUsable()
+
+ thread TitanKneel( npcTitan )
+}
+
+
+//////////////////////////////////////////////////////////
+function SetupAutoTitan( entity npcTitan, entity player )
+{
+ #if SP
+ npcTitan.SetUsePrompts( "#HOLD_TO_EMBARK_SP", "#PRESS_TO_EMBARK_SP" )
+ #endif
+
+ #if MP
+ npcTitan.SetUsePrompts( "#HOLD_TO_EMBARK", "#PRESS_TO_EMBARK" )
+ #endif
+
+ npcTitan.SetUsableByGroup( "owner pilot" )
+
+ NPCTitanFollowPilotInit( npcTitan, player )
+
+ NPCTitanGuardModeInit( npcTitan )
+
+ npcTitan.SetEnemyChangeCallback( AutoTitanChangedEnemy )
+
+ NPCTitanEnableCurrentMode( npcTitan, player )
+
+ npcTitan.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+ npcTitan.EnableNPCFlag( NPC_NEW_ENEMY_FROM_SOUND )
+ UpdateEnemyMemoryFromTeammates( npcTitan )
+
+ SetPlayerPetTitan( player, npcTitan )
+
+ SetupNPC_TitanTitle( npcTitan, player )
+
+ ShowName( npcTitan )
+
+ SPMP_UpdateNPCProficiency( npcTitan )
+}
+
+function SetPlayerPetTitan( entity player, entity npcTitan )
+{
+ if ( npcTitan == player.GetPetTitan() )
+ return
+
+ entity previousOwner = GetPetTitanOwner( npcTitan )
+ if ( IsValid( previousOwner ) )
+ {
+ previousOwner.SetPetTitan( null )
+ }
+
+ if ( IsAlive( player.GetPetTitan() ) )
+ {
+ Assert( !player.s.replacementDropInProgress, "Tried to give us a titan when we were executing a Titanfall" )
+ // kill old pet titan
+ player.GetPetTitan().Die( null, null, { scriptType = DF_INSTANT, damageSourceId = damagedef_suicide } )
+ }
+
+ // HACK: not really a hack, but this could be optimized to only render always for a given client
+ npcTitan.EnableRenderAlways()
+ player.SetPetTitan( npcTitan )
+ #if HAS_TITAN_EARNING
+ ClearTitanAvailable( player )
+ #endif
+ SetTeam( npcTitan, player.GetTeam() )
+ entity soul = npcTitan.GetTitanSoul()
+ if ( soul == null )
+ soul = player.GetTitanSoul()
+
+ string settings = GetSoulPlayerSettings( soul )
+ var maintainTitle = Dev_GetPlayerSettingByKeyField_Global( settings, "keep_title_on_autotitan" )
+ if ( maintainTitle != null && maintainTitle == 1 )
+ {
+ string title = expect string( GetPlayerSettingsFieldForClassName( settings, "printname" ) )
+ npcTitan.SetTitle( title )
+ }
+ else if ( SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ {
+ npcTitan.SetTitle( "#NPC_AUTO_TITAN_ENHANCED" )
+ }
+ else
+ {
+ npcTitan.SetTitle( "#NPC_AUTO_TITAN" )
+ }
+
+ npcTitan.DisableHibernation()
+}
+
+
+//////////////////////////////////////////////////////////
+function NPCTitanFollowPilotInit( npcTitan, player )
+{
+ int followBehavior = GetDefaultNPCFollowBehavior( npcTitan )
+ npcTitan.InitFollowBehavior( player, followBehavior )
+
+ if ( IsMultiplayer() )
+ {
+ npcTitan.SetFollowGoalTolerance( 700 )
+ npcTitan.SetFollowGoalCombatTolerance( 700 )
+ npcTitan.SetFollowTargetMoveTolerance( 200 )
+ }
+ else
+ {
+ npcTitan.SetFollowGoalTolerance( 500 )
+ npcTitan.SetFollowGoalCombatTolerance( 1200 )
+ npcTitan.SetFollowTargetMoveTolerance( 150 )
+ }
+
+ npcTitan.EnableBehavior( "Follow" )
+ npcTitan.DisableBehavior( "Assault" )
+}
+
+//////////////////////////////////////////////////////////
+function NPCTitanGuardModeInit( npcTitan )
+{
+#if DEV // Bug 110047
+ Assert( IsValid( npcTitan ) )
+ if ( !npcTitan.IsTitan() && !npcTitan.IsNPC() )
+ printl( "npcTitan is " + npcTitan.GetClassName() )
+#endif
+
+ npcTitan.AssaultSetFightRadius( 0 )
+
+ if ( IsSingleplayer() )
+ {
+ npcTitan.AssaultSetGoalRadius( 512 )
+ npcTitan.AssaultSetArrivalTolerance( 300 )
+ }
+ else
+ {
+ npcTitan.AssaultSetGoalRadius( 400 )
+ npcTitan.AssaultSetArrivalTolerance( 200 )
+ }
+}
+
+//////////////////////////////////////////////////////////
+function NPCTitanInitModeOnPlayerRespawn( player )
+{
+ if ( IsValid( player.GetPetTitan() ) )
+ {
+ local titan = player.GetPetTitan()
+
+ switch ( player.GetPetTitanMode() )
+ {
+ case eNPCTitanMode.FOLLOW:
+ NPCTitanFollowPilotInit( titan, player )
+ break;
+
+ default:
+ // nothing to do for other modes
+ break;
+ }
+ }
+}
+
+//////////////////////////////////////////////////////////
+function CodeCallback_PlayerRequestClimbInNPCTitan( npcTitan, player )
+{
+}
+
+
+
+
+//////////////////////////////////////////////////////////
+entity function CreateNPCTitanFromSettings( string settings, int team, vector origin, vector angles )
+{
+ entity npc = CreateNPCTitan( settings, team, origin, angles )
+ DispatchSpawn( npc )
+ return npc
+}
+
+function CreateTitanModelAndSkinSetup( entity npc )
+{
+ asset currentModel = npc.GetModelName()
+
+ if ( IsSingleplayer() )
+ {
+ switch ( currentModel )
+ {
+ case $"":
+ case $"models/titans/buddy/titan_buddy.mdl":
+ case $"models/titans/light/sp_titan_light_locust.mdl":
+ case $"models/titans/light/sp_titan_light_raptor.mdl":
+ case $"models/titans/heavy/sp_titan_heavy_deadbolt.mdl":
+ case $"models/titans/heavy/sp_titan_heavy_ogre.mdl":
+ case $"models/titans/medium/sp_titan_medium_ajax.mdl":
+ case $"models/titans/medium/sp_titan_medium_wraith.mdl":
+ break
+
+ default:
+ CodeWarning( "NPC titan at " + npc.GetOrigin() + " had non-sp titan model " + currentModel )
+ break
+ }
+ }
+
+ string settings = npc.ai.titanSettings.titanSetFile
+ asset model = GetPlayerSettingsAssetForClassName( settings, "bodymodel" )
+ npc.SetValueForModelKey( model )
+}
+
+// NEW TITAN STUFF BROUGHT OVER FROM TOWER DEFENSE R1
+string function GetRandomTitanWeapon()
+{
+ TitanLoadoutDef loadout = GetAllowedTitanLoadouts().getrandom()
+ return loadout.primary
+}
+
+void function ResetTitanBuildTime( entity player )
+{
+ if ( player.IsTitan() )
+ {
+ player.SetTitanBuildTime( GetCoreBuildTime( player ) )
+ return
+ }
+
+ player.SetTitanBuildTime( GetTitanBuildTime( player ) )
+}
+
+
+/* SP */
+
+void function SpawnTitanBattery( entity batteryRef )
+{
+ vector origin = batteryRef.GetOrigin()
+ entity battery = CreateTitanBattery( origin )
+ batteryRef.Destroy()
+}
+
+void function SpawnTitanBatteryOnDeath( entity titan, var damageInfo )
+{
+ if ( !titan.ai.shouldDropBattery || titan.GetTeam() == TEAM_MILITIA )
+ return
+ // if ( RandomFloatRange( 0, 100 ) < 50 )
+ // return
+ int attachID = titan.LookupAttachment( "CHESTFOCUS" )
+ vector origin = titan.GetAttachmentOrigin( attachID )
+
+ int numBatt = 0
+
+ if ( titan.IsTitan() && titan.ai.bossTitanType == TITAN_MERC )
+ {
+ numBatt = BATTERY_DROP_BOSS
+ }
+ else
+ {
+ if ( Flag( "PlayerDidSpawn" ) )
+ {
+ entity player = GetPlayerArray()[0]
+ entity playerTitan = GetTitanFromPlayer( player )
+
+ if ( IsValid( playerTitan ) &&
+ (
+ GetDoomedState( playerTitan ) ||
+ RandomDropBatteryBasedOnHealth( playerTitan )
+ )
+ )
+ {
+ numBatt = 1
+ }
+ }
+ }
+
+ for ( int i=0; i<numBatt; i++ )
+ {
+ vector vec = RandomVec( 150 )
+ if ( numBatt == 1 )
+ vec = < 0,0,0 >
+ entity battery = CreateTitanBattery( origin )
+ battery.SetVelocity( < vec.x, vec.y, 400 > )
+ }
+}
+
+entity function CreateTitanBattery( vector origin )
+{
+ entity battery = Rodeo_CreateBatteryPack()
+ battery.SetOrigin( origin )
+ //Highlight_SetNeutralHighlight( battery, "power_up" )
+ // if ( IsValid( battery ) )
+ // {
+ // PickupGlow glow = CreatePickupGlow( battery, 0, 255, 0 )
+ // glow.glowFX.SetParent( battery, "", true, 0 )
+ // }
+ return battery
+}
+
+void function SetWeaponCooldowns( entity player, array<entity> weapons, float cooldown )
+{
+ foreach ( weapon in weapons )
+ {
+ int max = weapon.GetWeaponPrimaryClipCountMax()
+ if ( max <= 0 )
+ continue
+ int current = int( max * cooldown )
+ weapon.SetWeaponPrimaryClipCountAbsolute( current )
+
+ if ( weapon.IsChargeWeapon() )
+ {
+ float chargeCooldownTime = weapon.GetWeaponSettingFloat( eWeaponVar.charge_cooldown_time )
+ if ( chargeCooldownTime > 1.0 )
+ {
+ weapon.SetWeaponPrimaryClipCountAbsolute( max )
+ weapon.SetWeaponChargeFractionForced( 1.0 - cooldown )
+ }
+ }
+ }
+}
+
+void function ResetTitanLoadoutFromPrimary( entity titan )
+{
+ Assert( titan.IsTitan() )
+ Assert( IsAlive( titan ) )
+
+// EmitSoundOnEntity( player, "Coop_AmmoBox_AmmoRefill" )
+ entity soul = titan.GetTitanSoul()
+ // not a real titan, swapping in/out of titan etc
+ if ( soul == null )
+ return
+
+ array<entity> weapons = GetPrimaryWeapons( titan )
+
+ foreach ( weapon in weapons )
+ {
+ TitanLoadoutDef ornull titanLoadout = GetTitanLoadoutForPrimary( weapon.GetWeaponClassName() )
+ if ( titanLoadout == null )
+ continue
+ expect TitanLoadoutDef( titanLoadout )
+
+ float coreValue = SoulTitanCore_GetNextAvailableTime( soul )
+
+ ReplaceTitanLoadoutWhereDifferent( titan, titanLoadout )
+
+ SoulTitanCore_SetNextAvailableTime( soul, coreValue )
+
+ if ( titan.IsPlayer() )
+ {
+// Remote_CallFunction_Replay( titan, "ServerCallback_NotifyLoadout", titan.GetEncodedEHandle() )
+ Remote_CallFunction_Replay( titan, "ServerCallback_UpdateTitanModeHUD" )
+ }
+ break
+ }
+}
+
+void function WaitForHotdropToEnd( entity titan )
+{
+ // Wait until player sees the boss titan
+ while ( titan.e.isHotDropping )
+ {
+ WaitFrame()
+ }
+}
+
+bool function RandomDropBatteryBasedOnHealth( entity playerTitan )
+{
+ float healthFrac = GetHealthFrac( playerTitan )
+ int randomPercent = RandomIntRange( 0, 100 )
+
+ if ( healthFrac <= BATTERY_DROP_HEALTH_FRAC_SURE )
+ {
+ return true
+ }
+ else if ( healthFrac <= BATTERY_DROP_HEALTH_FRAC_MID )
+ {
+ return randomPercent <= BATTERY_DROP_MID_CHANCE
+ }
+ else
+ {
+ return randomPercent <= BATTERY_DROP_LOW_CHANCE
+ }
+
+ return false
+}
+
+void function TitanAchievementTracking_SP( entity titan, var damageInfo )
+{
+ entity player = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !titan.IsTitan() )
+ return
+
+ if ( !IsValid( player ) )
+ return
+
+ if ( !player.IsPlayer() )
+ return
+
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ switch ( damageSourceId )
+ {
+ case eDamageSourceId.mp_titancore_salvo_core:
+ case eDamageSourceId.mp_titancore_laser_cannon:
+ case eDamageSourceId.mp_titancore_flame_wave:
+ case eDamageSourceId.mp_titancore_flame_wave_secondary:
+ case eDamageSourceId.mp_titancore_shift_core:
+ case eDamageSourceId.mp_titanweapon_flightcore_rockets:
+ case eDamageSourceId.mp_titancore_amp_core:
+ file.coreKillCounter++
+ break
+ case eDamageSourceId.mp_titanweapon_predator_cannon:
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+ if ( weaponMods.contains( "Smart_Core" ) )
+ {
+ file.coreKillCounter++
+ }
+ break
+ #if HAS_BOSS_AI
+ case eDamageSourceId.titan_execution:
+ if ( IsMercTitan( titan ) )
+ {
+ UnlockAchievement( player, achievements.EXECUTE_BOSS )
+ }
+ break
+ #endif
+ }
+
+ if ( file.coreKillCounter >= 3 )
+ {
+ UnlockAchievement( player, achievements.CORE_MULTIKILL )
+ }
+
+ if ( !player.IsTitan() )
+ {
+ UnlockAchievement( player, achievements.PILOT_TITANKILL )
+ }
+
+ // don't count vortex refire for core kills
+ int scriptDamageType = DamageInfo_GetCustomDamageType( damageInfo )
+ if ( scriptDamageType & DF_VORTEX_REFIRE )
+ return
+
+ switch ( damageSourceId )
+ {
+ case eDamageSourceId.mp_titancore_salvo_core:
+ UnlockAchievement( player, achievements.CORE_SALVO )
+ break
+ case eDamageSourceId.mp_titancore_laser_cannon:
+ UnlockAchievement( player, achievements.CORE_LASER )
+ break
+ case eDamageSourceId.mp_titancore_flame_wave:
+ case eDamageSourceId.mp_titancore_flame_wave_secondary:
+ UnlockAchievement( player, achievements.CORE_FLAME )
+ break
+ case eDamageSourceId.mp_titancore_shift_core:
+ UnlockAchievement( player, achievements.CORE_SWORD )
+ break
+ case eDamageSourceId.mp_titanweapon_flightcore_rockets:
+ UnlockAchievement( player, achievements.CORE_FLIGHT )
+ break
+ case eDamageSourceId.mp_titancore_amp_core:
+ UnlockAchievement( player, achievements.CORE_BURST )
+ break
+ case eDamageSourceId.mp_titanweapon_predator_cannon:
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+ if ( weaponMods.contains( "Smart_Core" ) )
+ {
+ UnlockAchievement( player, achievements.CORE_SMART )
+ }
+ break
+ }
+}
+
+// this gets called whenever a core is started
+void function ResetCoreKillCounter()
+{
+ file.coreKillCounter = 0
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_titan_tether.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_titan_tether.gnut
new file mode 100644
index 000000000..b088651ab
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_titan_tether.gnut
@@ -0,0 +1,307 @@
+global function TitanTether_Init
+global function AddTitanTether
+global function TetherFlyIn
+global function PROTO_GetActiveTethers
+global function CodeCallback_OnTetherRemove
+global function CodeCallback_OnTetherDamageMilestone
+global function AddOnTetherCallback
+
+struct TetherData
+{
+ entity owner
+ entity[2] endpointEnts
+ array<entity> tetherEnts = []
+ entity anchor
+ entity endEntForPlayer
+ entity endEntForOthers
+ int teamNum
+ int codeTetherID
+}
+
+
+struct
+{
+ array<TetherData> activeTitanTethers = []
+ array< void functionref( entity, entity ) > onTetherCallbacks = []
+} file
+
+void function TitanTether_Init()
+{
+ PrecacheImpactEffectTable( "exp_tether_trap" ) //Needs to match damagedef_fd_tether_trap
+}
+
+void function AddOnTetherCallback( void functionref( entity, entity ) callback )
+{
+ file.onTetherCallbacks.append( callback )
+}
+
+void function AddTitanTether( entity owner, entity startEnt, entity endEnt, array<entity> tetherEnts, entity anchor, entity tetherEndEntForPlayer, entity tetherEndEntForOthers, bool isExplosiveTether )
+{
+ //Run callbacks for tether trap activation.
+ foreach ( callback in file.onTetherCallbacks )
+ {
+ callback( owner, endEnt )
+ }
+
+ TetherData tetherData
+ tetherData.owner = owner
+
+ tetherData.teamNum = owner.GetTeam()
+
+ Assert( !startEnt.IsTitan() )
+ Assert( endEnt.IsTitan()|| IsSuperSpectre( endEnt ) )
+
+ if ( endEnt.IsTitan() || IsSuperSpectre( endEnt ) )
+ {
+ tetherData.codeTetherID = endEnt.AddTether( startEnt.GetOrigin() )
+ if ( owner.IsPlayer() )
+ EmitSoundOnEntityExceptToPlayer( startEnt, owner, "Wpn_TetherTrap_PopOpen_3p" )//Spring Sound
+ else
+ EmitSoundOnEntity( startEnt, "Wpn_TetherTrap_PopOpen_3p" )//Spring Sound
+
+ if ( endEnt.IsTitan() )
+ endEnt = endEnt.GetTitanSoul()
+ }
+
+ tetherData.endpointEnts[0] = startEnt
+ tetherData.endpointEnts[1] = endEnt
+
+ tetherData.tetherEnts = tetherEnts
+
+ tetherData.anchor = anchor
+ tetherData.endEntForPlayer = tetherEndEntForPlayer
+ tetherData.endEntForOthers = tetherEndEntForOthers
+
+ file.activeTitanTethers.append( tetherData )
+
+ thread TetherCleanup( owner, startEnt, endEnt, tetherData, isExplosiveTether )
+}
+
+void function TetherCleanup( entity owner, entity startEnt, entity endEnt, TetherData tetherData, bool isExplosiveTether )
+{
+ startEnt.EndSignal( "OnDestroy" )
+ endEnt.EndSignal( "OnDestroy" )
+ endEnt.EndSignal( "OnSyncedMelee" )
+
+ int tetherID = tetherData.codeTetherID
+// int statusEffectId = StatusEffect_AddEndless( endEnt, eStatusEffect.tethered, 1.0 )
+ int statusEffectId = StatusEffect_AddTimed( endEnt, eStatusEffect.tethered, 1.0, 5.0, 0.0 )
+
+ vector anchorOrigin = tetherData.anchor.GetOrigin()
+
+ OnThreadEnd(
+ function() : ( owner, anchorOrigin, endEnt, tetherID, statusEffectId, isExplosiveTether )
+ {
+ if ( isExplosiveTether && IsValid( owner ) && IsValid( endEnt ) )
+ {
+ Explosion_DamageDefSimple( damagedef_fd_tether_trap, anchorOrigin,owner, owner, anchorOrigin + < 0, 0, 32 > )
+ }
+
+ foreach ( index, tetherData in file.activeTitanTethers )
+ {
+ if ( tetherData.codeTetherID == tetherID )
+ {
+ thread TitanTether_Remove( tetherData )
+ break
+ }
+ }
+
+ if ( IsValid( endEnt ) )
+ StatusEffect_Stop( endEnt, statusEffectId )
+ }
+ )
+
+ WaitForever()
+}
+
+void function TetherFlyIn( entity flyFrom, entity flyTo, entity rope, entity owner )
+{
+ flyTo.EndSignal( "OnDestroy" )
+ vector destLocal = flyTo.GetLocalOrigin()
+ flyTo.SetAbsOrigin( flyFrom.GetOrigin() )
+ flyTo.NonPhysicsMoveInWorldSpaceToLocalPos( destLocal, 0.3, 0, 0 )
+ wait 0.3
+ if ( IsValid( owner ) && owner.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( flyTo, owner, "Weapon_TetherGun_Attach_1P_VS_3P" )
+ EmitSoundOnEntityExceptToPlayer( flyTo, owner, "Weapon_TetherGun_Attach_3P_VS_3P" )
+ }
+ else
+ {
+ EmitSoundOnEntity( flyTo, "Weapon_TetherGun_Attach_3P_VS_3P" )
+ }
+}
+
+
+void function TitanTether_Remove( TetherData tetherData )
+{
+ entity endEnt = tetherData.endpointEnts[1]
+ if ( IsValid( endEnt ) )
+ {
+ if ( IsSoul( endEnt ) )
+ endEnt = endEnt.GetTitan()
+
+ if ( endEnt.IsValidTetherID( tetherData.codeTetherID ) )
+ endEnt.RemoveTether( tetherData.codeTetherID )
+ }
+
+ vector angvel = < RandomFloatRange( 50, 1000 ), RandomFloatRange( -200, 200 ), RandomFloatRange( -200, 200 )>
+
+ vector velForPlayer
+ vector velForOthers
+ vector rotaxis
+ float rotspeed
+ if ( IsValid( endEnt ) )
+ {
+ vector forward = endEnt.GetPlayerOrNPCViewVector()
+ velForPlayer = forward * 200
+
+ rotaxis = endEnt.GetPlayerOrNPCViewRight()
+ rotaxis += forward * RandomFloatRange( -0.4, 0.4 )
+ rotaxis += endEnt.GetPlayerOrNPCViewUp() * RandomFloatRange( -0.4, 0.4 )
+ rotspeed = RandomFloatRange( -2000, -1000 )
+ }
+ else
+ {
+ rotaxis = RandomVec( 1 )
+ rotspeed = RandomFloatRange( -2000, 2000 )
+ }
+
+ vector pullDirForPlayer
+ vector pullDirForOthers
+
+ bool endEntForPlayerIsValid = IsValid( tetherData.endEntForPlayer )
+ bool endEntForOthersIsValid = IsValid( tetherData.endEntForOthers )
+
+ if ( IsValid( tetherData.anchor ) )
+ {
+ if ( endEntForPlayerIsValid )
+ pullDirForPlayer = Normalize( tetherData.anchor.GetOrigin() - tetherData.endEntForPlayer.GetOrigin() )
+ if ( endEntForOthersIsValid )
+ pullDirForOthers = Normalize( tetherData.anchor.GetOrigin() - tetherData.endEntForOthers.GetOrigin() )
+ }
+
+ velForPlayer += < RandomFloatRange(-100,100), RandomFloatRange(-100,100), 0> + pullDirForPlayer * 100
+ float pullDirForOthersZ = pullDirForOthers.z
+ pullDirForOthers.z = 0
+ pullDirForOthers += < RandomFloatRange(-1,1), RandomFloatRange(-1,1), 0>
+ velForOthers = Normalize( pullDirForOthers )
+ velForOthers.x *= RandomFloatRange( 100, 200 )
+ velForOthers.y *= RandomFloatRange( 100, 200 )
+ velForOthers.z = pullDirForOthersZ * 200
+
+ rotaxis = Normalize( rotaxis )
+
+ // Since we're unparenting the tethers, we need to change how they control who they're visible to
+ if ( IsValid( endEnt ) )
+ {
+ if ( endEntForPlayerIsValid )
+ {
+ tetherData.endEntForPlayer.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ tetherData.endEntForPlayer.SetOwner( endEnt )
+ }
+ if ( endEntForOthersIsValid )
+ {
+ tetherData.endEntForOthers.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY
+ tetherData.endEntForOthers.SetOwner( endEnt )
+ }
+ }
+ else
+ {
+ if ( endEntForPlayerIsValid )
+ tetherData.endEntForPlayer.kv.VisibilityFlags = ENTITY_VISIBLE_TO_NOBODY
+ if ( endEntForOthersIsValid )
+ tetherData.endEntForOthers.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ }
+
+ if ( endEntForPlayerIsValid )
+ {
+ tetherData.endEntForPlayer.ClearParent()
+ tetherData.endEntForPlayer.NonPhysicsRotate( rotaxis, rotspeed )
+ tetherData.endEntForPlayer.NonPhysicsMoveWithGravity( velForPlayer, < 0, 0, -750> )
+ tetherData.endEntForPlayer.RenderWithViewModels( false )
+ tetherData.endEntForPlayer.Dissolve( ENTITY_DISSOLVE_NORMAL, <0,0,0>, 100 )
+ }
+
+ if ( endEntForOthersIsValid )
+ {
+ tetherData.endEntForOthers.ClearParent()
+ tetherData.endEntForOthers.NonPhysicsRotate( rotaxis, rotspeed )
+ tetherData.endEntForOthers.NonPhysicsMoveWithGravity( velForOthers, < 0, 0, -750> )
+ tetherData.endEntForOthers.Dissolve( ENTITY_DISSOLVE_NORMAL, <0,0,0>, 100 )
+ }
+
+ wait 0.5
+
+ foreach ( index, ent in tetherData.endpointEnts )
+ {
+ if ( !IsValid( ent ) )
+ continue
+
+ if ( ent instanceof CBaseGrenade )
+ ent.Destroy()
+ }
+
+ foreach ( entity ent in tetherData.tetherEnts )
+ {
+ if ( IsValid( ent ) )
+ ent.Destroy()
+ }
+}
+
+
+int function PROTO_GetActiveTethers( entity owner )
+{
+// _PruneActiveTitanTethers()
+
+ int activeTethers = 0
+ foreach ( TetherData tetherData in file.activeTitanTethers )
+ {
+ if ( tetherData.owner == owner )
+ activeTethers++
+ }
+
+ return activeTethers
+}
+
+
+bool removingTether
+
+void function CodeCallback_OnTetherRemove( entity guy, int tetherID )
+{
+ Assert( !removingTether )
+ removingTether = true
+
+ foreach ( index, tetherData in file.activeTitanTethers )
+ {
+ if ( tetherData.codeTetherID == tetherID )
+ {
+ thread TitanTether_Remove( tetherData )
+ file.activeTitanTethers.fastremove( index )
+ break
+ }
+ }
+
+ removingTether = false
+}
+
+TetherData function GetTetherDataForCodeID( int codeTetherID )
+{
+ foreach ( index, tetherData in file.activeTitanTethers )
+ {
+ if ( tetherData.codeTetherID == codeTetherID )
+ return tetherData
+ }
+
+ unreachable
+}
+
+void function CodeCallback_OnTetherDamageMilestone( entity guy, int tetherID, int damageMilestoneIndex, float health )
+{
+ float healthFrac = 1.0 - health / 1000.0
+
+ TetherData tetherData = GetTetherDataForCodeID( tetherID )
+
+ vector ang = tetherData.endEntForPlayer.GetLocalAngles()
+ tetherData.endEntForPlayer.SetLocalAngles( < healthFrac * 45, ang.y, ang.z> )
+}
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_titan_transfer.nut b/Northstar.CustomServers/scripts/vscripts/mp/_titan_transfer.nut
new file mode 100644
index 000000000..7b126cd0c
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_titan_transfer.nut
@@ -0,0 +1,641 @@
+untyped
+
+global function TitanTransfer_Init
+
+global function PilotBecomesTitan
+global function TitanBecomesPilot
+global function CreateAutoTitanForPlayer_ForTitanBecomesPilot
+global function CreateAutoTitanForPlayer_FromTitanLoadout
+global function CopyWeapons
+global function StorePilotWeapons
+global function RetrievePilotWeapons
+global function SetTitanSettings
+
+global function StoreWeapons
+global function GiveWeaponsFromStoredArray
+
+global function TitanCoreEffectTransfer_threaded
+global function ForceTitanSustainedDischargeEnd
+
+function TitanTransfer_Init()
+{
+ // these vars transfer from player titan to npc titan and vice versa
+
+ RegisterSignal( "PlayerEmbarkedTitan" )
+ RegisterSignal( "PlayerDisembarkedTitan" )
+
+ AddSoulTransferFunc( TitanCoreEffectTransfer )
+ AddCallback_OnTitanBecomesPilot( OnClassChangeBecomePilot )
+ AddCallback_OnPilotBecomesTitan( OnClassChangeBecomeTitan )
+}
+
+void function TitanCoreEffectTransfer( entity soul, entity titan, entity oldTitan )
+{
+ thread TitanCoreEffectTransfer_threaded( soul, titan, oldTitan )
+}
+function TitanCoreEffectTransfer_threaded( entity soul, entity titan, entity oldTitan )
+{
+ WaitEndFrame() // because the titan aint a titan yet
+
+ if ( !IsValid( soul ) || !IsValid( titan ) )
+ return
+
+ if ( !( "coreEffect" in soul.s ) )
+ return
+
+ soul.s.coreEffect.ent.Kill_Deprecated_UseDestroyInstead()
+ soul.s.coreEffect.ent = soul.s.coreEffect.func( titan, soul.s.coreEffect.parameter )
+}
+
+void function OnClassChangeBecomePilot( entity player, entity titan ) //Stuff here used to be in old CPlayer:OnChangedPlayerClass, for turning into Pilot class
+{
+ player.ClearDoomed()
+ player.UnsetUsable()
+ player.lastTitanTime = Time()
+
+ player.Minimap_SetHeightTracking( true )
+ ResetTitanBuildTime( player )
+ RandomizeHead( player )
+}
+
+void function OnClassChangeBecomeTitan( entity player, entity titan ) //Stuff here used to be in old CPlayer:OnChangedPlayerClass, for turning into Titan class
+{
+ CodeCallback_PlayerInTitanCockpit( player, player )
+ player.Minimap_SetHeightTracking( false )
+ ResetTitanBuildTime( player )
+}
+
+function CopyWeapons( entity fromEnt, entity toEnt )
+{
+ entity activeWeapon = fromEnt.GetActiveWeapon()
+ if ( IsValid( activeWeapon ) )
+ {
+ if ( activeWeapon.IsWeaponOffhand() )
+ fromEnt.ClearOffhand()
+ }
+
+ array<entity> weapons = fromEnt.GetMainWeapons()
+ foreach ( weapon in weapons )
+ {
+ entity giveWeapon = fromEnt.TakeWeapon_NoDelete( weapon.GetWeaponClassName() )
+ toEnt.GiveExistingWeapon( giveWeapon )
+ }
+
+ for ( int i = 0; i < OFFHAND_COUNT; i++ )
+ {
+ entity offhandWeapon
+ offhandWeapon = fromEnt.TakeOffhandWeapon_NoDelete( i )
+
+ // maintain offhand index
+ if ( offhandWeapon )
+ toEnt.GiveExistingOffhandWeapon( offhandWeapon, i )
+ }
+
+ if ( activeWeapon )
+ {
+ string name = activeWeapon.GetWeaponClassName()
+ toEnt.SetActiveWeaponByName( name )
+ }
+}
+
+array<StoredWeapon> function StoreWeapons( entity player )
+{
+ array<StoredWeapon> storedWeapons
+
+ entity activeWeapon = player.GetActiveWeapon()
+
+ array<entity> mainWeapons = player.GetMainWeapons()
+
+ foreach ( i, weapon in mainWeapons )
+ {
+ StoredWeapon sw
+
+ if ( weapon.GetScriptFlags0() & WEAPONFLAG_AMPED )
+ {
+ weapon.Signal( "StopAmpedWeapons" )
+ }
+
+ sw.name = weapon.GetWeaponClassName()
+ sw.weaponType = eStoredWeaponType.main
+ sw.activeWeapon = ( weapon == activeWeapon )
+ sw.inventoryIndex = i
+ sw.mods = weapon.GetMods()
+ sw.modBitfield = weapon.GetModBitField()
+ sw.ammoCount = weapon.GetWeaponPrimaryAmmoCount()
+ sw.clipCount = weapon.GetWeaponPrimaryClipCount()
+ sw.nextAttackTime = weapon.GetNextAttackAllowedTime()
+ sw.skinIndex = weapon.GetSkin()
+ sw.camoIndex = weapon.GetCamo()
+ sw.isProScreenOwner = weapon.GetProScreenOwner() == player
+
+ storedWeapons.append( sw )
+ }
+
+ array<entity> offhandWeapons = player.GetOffhandWeapons()
+
+ foreach ( weapon in offhandWeapons )
+ {
+ StoredWeapon sw
+
+ sw.name = weapon.GetWeaponClassName()
+ sw.weaponType = eStoredWeaponType.offhand
+ sw.activeWeapon = ( weapon == activeWeapon )
+ sw.inventoryIndex = weapon.GetInventoryIndex()
+ sw.mods = weapon.GetMods()
+ sw.ammoCount = weapon.GetWeaponPrimaryAmmoCount()
+ sw.clipCount = weapon.GetWeaponPrimaryClipCount()
+ sw.nextAttackTime = weapon.GetNextAttackAllowedTime()
+ #if MP
+ sw.burnReward = weapon.e.burnReward
+ #endif
+
+ if ( sw.activeWeapon )
+ storedWeapons.insert( 0, sw )
+ else
+ storedWeapons.append( sw )
+ }
+
+ return storedWeapons
+}
+
+void function GiveWeaponsFromStoredArray( entity player, array<StoredWeapon> storedWeapons )
+{
+ int activeWeaponSlot = 0
+ foreach ( i, storedWeapon in storedWeapons )
+ {
+ entity weapon
+
+ switch ( storedWeapon.weaponType )
+ {
+ case eStoredWeaponType.main:
+ weapon = player.GiveWeapon( storedWeapon.name, storedWeapon.mods )
+ weapon.SetWeaponSkin( storedWeapon.skinIndex )
+ weapon.SetWeaponCamo( storedWeapon.camoIndex )
+ #if MP
+ if ( storedWeapon.isProScreenOwner )
+ {
+ weapon.SetProScreenOwner( player )
+ UpdateProScreen( player, weapon )
+ }
+
+ string weaponCategory = GetWeaponInfoFileKeyField_GlobalString( weapon.GetWeaponClassName(), "menu_category" )
+ if ( weaponCategory == "at" || weaponCategory == "special" ) // refill AT/grenadier ammo stockpile
+ {
+ int defaultTotal = weapon.GetWeaponSettingInt( eWeaponVar.ammo_default_total )
+ int clipSize = weapon.GetWeaponSettingInt( eWeaponVar.ammo_clip_size )
+
+ weapon.SetWeaponPrimaryAmmoCount( defaultTotal - clipSize )
+
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( storedWeapon.clipCount )
+ }
+ else
+ {
+ weapon.SetWeaponPrimaryAmmoCount( storedWeapon.ammoCount )
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( storedWeapon.clipCount )
+ }
+ #else
+ weapon.SetWeaponPrimaryAmmoCount( storedWeapon.ammoCount )
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( storedWeapon.clipCount )
+ #endif
+
+
+ if ( storedWeapon.activeWeapon )
+ activeWeaponSlot = i
+
+ break
+
+ case eStoredWeaponType.offhand:
+ player.GiveOffhandWeapon( storedWeapon.name, storedWeapon.inventoryIndex, storedWeapon.mods )
+
+ weapon = player.GetOffhandWeapon( storedWeapon.inventoryIndex )
+ weapon.SetNextAttackAllowedTime( storedWeapon.nextAttackTime )
+ weapon.SetWeaponPrimaryAmmoCount( storedWeapon.ammoCount )
+ if ( weapon.GetWeaponPrimaryClipCountMax() > 0 )
+ weapon.SetWeaponPrimaryClipCount( storedWeapon.clipCount )
+ #if MP
+ weapon.e.burnReward = storedWeapon.burnReward
+ #endif
+
+ break
+
+ default:
+ unreachable
+ }
+ }
+
+ #if MP
+ PlayerInventory_RefreshEquippedState( player )
+ #endif
+
+ player.SetActiveWeaponBySlot( activeWeaponSlot )
+}
+
+void function RetrievePilotWeapons( entity player )
+{
+ TakeAllWeapons( player )
+ GiveWeaponsFromStoredArray( player, player.p.storedWeapons )
+ SetPlayerCooldowns( player )
+ player.p.storedWeapons.clear()
+}
+
+function StorePilotWeapons( entity player )
+{
+ player.p.storedWeapons = StoreWeapons( player )
+ StoreOffhandData( player, false )
+ TakeAllWeapons( player )
+}
+
+function TransferHealth( srcEnt, destEnt )
+{
+ destEnt.SetMaxHealth( srcEnt.GetMaxHealth() )
+ destEnt.SetHealth( srcEnt.GetHealth() )
+ //destEnt.SetHealthPerSegment( srcEnt.GetHealthPerSegment() )
+}
+
+entity function CreateAutoTitanForPlayer_FromTitanLoadout( entity player, TitanLoadoutDef loadout, vector origin, vector angles )
+{
+ int team = player.GetTeam()
+
+ player.titansBuilt++
+ ResetTitanBuildTime( player )
+
+ entity npcTitan = CreateNPCTitan( loadout.setFile, team, origin, angles, loadout.setFileMods )
+ SetTitanSpawnOptionsFromLoadout( npcTitan, loadout )
+ SetSpawnOption_OwnerPlayer( npcTitan, player )
+
+ if ( IsSingleplayer() )
+ {
+ npcTitan.EnableNPCFlag( NPC_IGNORE_FRIENDLY_SOUND )
+ }
+ #if MP
+ string titanRef = GetTitanCharacterNameFromSetFile( loadout.setFile )
+ npcTitan.SetTargetInfoIcon( GetTitanCoreIcon( titanRef ) )
+ #endif
+
+ return npcTitan
+}
+
+entity function CreateAutoTitanForPlayer_ForTitanBecomesPilot( entity player, bool hidden = false )
+{
+ vector origin = player.GetOrigin()
+ vector angles = player.GetAngles()
+ TitanLoadoutDef loadout = GetTitanLoadoutFromPlayerInventory( player )
+
+ int team = player.GetTeam()
+ entity npcTitan = CreateNPCTitan( loadout.setFile, team, origin, angles, loadout.setFileMods )
+ npcTitan.s.spawnWithoutSoul <- true
+ SetTitanSpawnOptionsFromLoadout( npcTitan, loadout )
+ SetSpawnOption_OwnerPlayer( npcTitan, player )
+ npcTitan.SetSkin( player.GetSkin() )
+ npcTitan.SetCamo( player.GetCamo() )
+ npcTitan.SetDecal( player.GetDecal() )
+
+ if ( IsSingleplayer() )
+ npcTitan.EnableNPCFlag( NPC_IGNORE_FRIENDLY_SOUND )
+
+ return npcTitan
+}
+
+void function SetTitanSpawnOptionsFromLoadout( entity titan, TitanLoadoutDef loadout )
+{
+ titan.ai.titanSpawnLoadout = loadout
+}
+
+TitanLoadoutDef function GetTitanLoadoutFromPlayerInventory( entity player ) //TODO: Examine necessity for this? Was needed in R1 where TItans could pick up weapons off the ground, but may not be needed in R2 anymore. Might just be fine to call GetTitanLoadoutForPlayer()
+{
+ TitanLoadoutDef loadout = GetTitanLoadoutForPlayer( player )
+ loadout.setFile = player.GetPlayerSettings()
+ loadout.setFileMods = UntypedArrayToStringArray( player.GetPlayerSettingsMods() )
+
+ array mainWeapons = player.GetMainWeapons()
+ if ( mainWeapons.len() )
+ {
+ entity wep = player.GetMainWeapons()[0]
+ loadout.primary = wep.GetWeaponClassName()
+ loadout.primaryMods = wep.GetMods()
+ }
+
+ entity ord = player.GetOffhandWeapon(OFFHAND_ORDNANCE)
+ if ( ord )
+ {
+ loadout.ordnance = ord.GetWeaponClassName()
+ loadout.ordnanceMods = ord.GetMods()
+ }
+
+ entity tac = player.GetOffhandWeapon(OFFHAND_SPECIAL)
+ if ( tac )
+ {
+ loadout.special = tac.GetWeaponClassName()
+ loadout.specialMods = tac.GetMods()
+ }
+
+ entity antirodeo = player.GetOffhandWeapon(OFFHAND_ANTIRODEO)
+ if ( antirodeo )
+ {
+ loadout.antirodeo = antirodeo.GetWeaponClassName()
+ loadout.antirodeoMods = antirodeo.GetMods()
+ }
+
+ entity melee = player.GetMeleeWeapon()
+ if ( melee )
+ loadout.melee = melee.GetWeaponClassName()
+
+ return loadout
+}
+
+void function ForceTitanSustainedDischargeEnd( entity player )
+{
+ // To disable core's while disembarking
+ local weapons = player.GetOffhandWeapons()
+ foreach ( weapon in weapons )
+ {
+ if ( weapon.IsChargeWeapon() )
+ weapon.ForceChargeEndNoAttack()
+
+ if ( weapon.IsSustainedDischargeWeapon() && weapon.IsDischarging() )
+ weapon.ForceSustainedDischargeEnd();
+ }
+}
+
+function TitanBecomesPilot( entity player, entity titan )
+{
+ Assert( IsAlive( player ), player + ": Player is not alive" )
+ Assert( player.IsTitan(), player + " is not a titan" )
+
+ Assert( IsAlive( titan ), titan + " is not alive." )
+ Assert( titan.IsTitan(), titan + " is not alive." )
+
+ asset model = player.GetModelName()
+ int skin = player.GetSkin()
+ int camo = player.GetCamo()
+ int decal = player.GetDecal()
+ titan.SetModel( model )
+ titan.SetSkin( skin )
+ titan.SetCamo( camo )
+ titan.SetDecal( decal )
+ titan.SetPoseParametersSameAs( player )
+ titan.SequenceTransitionFromEntity( player )
+
+ ForceTitanSustainedDischargeEnd( player )
+
+ TransferHealth( player, titan )
+ //Transfer children before player becomes pilot model
+ player.TransferChildrenTo( titan )
+ player.TransferTethersToEntity( titan )
+ entity soul = player.GetTitanSoul()
+ SetSoulOwner( soul, titan )
+ Assert( player.GetTitanSoul() == null )
+
+ // this must happen before changing the players settings
+ TransferDamageStates( player, titan )
+
+ // cant have a titan passive when you're not a titan
+ TakeAllTitanPassives( player )
+
+ player.SetPlayerSettingsWithMods( player.s.storedPlayerSettings, player.s.storedPlayerSettingsMods )
+ player.SetPlayerSettingPosMods( PLAYERPOSE_STANDING, player.s.storedPlayerStandMods )
+ player.SetPlayerSettingPosMods( PLAYERPOSE_CROUCHING, player.s.storedPlayerCrouchMods )
+ player.SetSkin( player.s.storedPlayerSkinIndex )
+ player.SetCamo( player.s.storedPlayerCamoIndex )
+
+ delete player.s.storedPlayerSettings
+ delete player.s.storedPlayerSettingsMods
+ delete player.s.storedPlayerStandMods
+ delete player.s.storedPlayerCrouchMods
+ delete player.s.storedPlayerSkinIndex
+ delete player.s.storedPlayerCamoIndex
+
+ TakeAllWeapons( titan )
+ CopyWeapons( player, titan )
+
+ player.SetTitle( "" )
+
+ RetrievePilotWeapons( player )
+
+ if ( Riff_AmmoLimit() != eAmmoLimit.Default )
+ {
+ switch ( Riff_AmmoLimit() )
+ {
+ case eAmmoLimit.Limited:
+ local weapons = player.GetMainWeapons()
+ foreach ( weapon in weapons )
+ {
+ local clipAmmo = player.GetWeaponAmmoMaxLoaded( weapon )
+
+ if ( clipAmmo > 0 )
+ weapon.SetWeaponPrimaryAmmoCount( clipAmmo * 2 )
+ }
+
+ local offhand = player.GetOffhandWeapon( 0 )
+ if ( offhand )
+ {
+ local ammoLoaded = player.GetWeaponAmmoMaxLoaded( offhand )
+ offhand.SetWeaponPrimaryClipCount( max( 1, ammoLoaded - 2 ) )
+ }
+ break
+ }
+ }
+
+ // Added via AddCallback_OnTitanBecomesPilot
+ foreach ( callbackFunc in svGlobal.onTitanBecomesPilotCallbacks )
+ {
+ callbackFunc( player, titan )
+ }
+
+ // Ensure rodeo doesn't happen straight away, if a nearby Titan runs by
+ Rodeo_SetCooldown( player )
+
+ if ( player.cloakedForever )
+ {
+ // infinite cloak active
+ EnableCloakForever( player )
+ }
+ if ( player.stimmedForever )
+ {
+ StimPlayer( player, USE_TIME_INFINITE )
+ }
+
+ soul.Signal( "PlayerDisembarkedTitan", { player = player } )
+
+ // no longer owned
+ if ( soul.capturable )
+ {
+ soul.ClearBossPlayer()
+ titan.ClearBossPlayer()
+ titan.SetUsableByGroup( "friendlies pilot" )
+ titan.DisableBehavior( "Follow" )
+ player.SetPetTitan( null )
+ if ( !player.s.savedTitanBuildTimer )
+ ResetTitanBuildTime( player )
+ else
+ player.SetNextTitanRespawnAvailable( player.s.savedTitanBuildTimer )
+ return
+ }
+ return titan
+}
+
+function PilotBecomesTitan( entity player, entity titan, bool fullCopy = true )
+{
+ player.SetPetTitan( null )
+
+ // puts the weapons into a table
+ StorePilotWeapons( player )
+
+ #if HAS_TITAN_WEAPON_SWAPPING
+ {
+ foreach ( weapon in titan.GetMainWeapons() )
+ {
+ // the pilot's weapons will recent entirely in sp if this doesn't match
+ player.p.lastPrimaryWeaponEnt = weapon
+ break
+ }
+ }
+ #endif
+
+ if ( fullCopy )
+ {
+ CopyWeapons( titan, player )
+ }
+
+ //Should only be the first time a player embarks into a Titan that Titan's life.
+ //Check with differ if there is a better way than .e.var on the soul.
+ //TitanLoadoutDef loadout = GetTitanLoadoutForPlayer( player )
+ //PROTO_DisplayTitanLoadouts( player, titan, loadout )
+
+ entity soul = titan.GetTitanSoul()
+ soul.soul.lastOwner = player
+
+ player.s.storedPlayerSettings <- player.GetPlayerSettings()
+ player.s.storedPlayerSettingsMods <- player.GetPlayerSettingsMods()
+ player.s.storedPlayerStandMods <- player.GetPlayerModsForPos( PLAYERPOSE_STANDING )
+ player.s.storedPlayerCrouchMods <- player.GetPlayerModsForPos( PLAYERPOSE_CROUCHING )
+ player.s.storedPlayerSkinIndex <- player.GetSkin()
+ player.s.storedPlayerCamoIndex <- player.GetCamo()
+ printt( player.GetSkin(), player.GetCamo() )
+
+ string settings = GetSoulPlayerSettings( soul )
+ var titanTint = Dev_GetPlayerSettingByKeyField_Global( settings, "titan_tint" )
+
+ if ( titanTint != null )
+ {
+ expect string( titanTint )
+ Highlight_SetEnemyHighlight( player, titanTint )
+ }
+ else
+ {
+ Highlight_ClearEnemyHighlight( player )
+ }
+
+ if ( !player.GetParent() )
+ {
+ player.SetOrigin( titan.GetOrigin() )
+ player.SetAngles( titan.GetAngles() )
+ player.SetVelocity( Vector( 0,0,0 ) )
+ }
+
+ if ( soul.capturable )
+ {
+ printt( player.GetPetTitan(), player.GetNextTitanRespawnAvailable() )
+ if ( IsValid( player.GetPetTitan() ) || player.s.replacementDropInProgress )
+ player.s.savedTitanBuildTimer <- null
+ else
+ player.s.savedTitanBuildTimer <- player.GetNextTitanRespawnAvailable()
+
+ if ( GameRules_GetGameMode() == "ctt" )
+ {
+ titan.Minimap_AlwaysShow( 0, null )
+ player.Minimap_AlwaysShow( TEAM_IMC, null )
+ player.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ player.SetHudInfoVisibilityTestAlwaysPasses( true )
+ }
+ }
+
+ SetSoulOwner( soul, player )
+
+ if ( soul.GetBossPlayer() != player )
+ SoulBecomesOwnedByPlayer( soul, player )
+
+ foreach ( int passive, _ in level.titanPassives )
+ {
+ if ( SoulHasPassive( soul, passive ) )
+ {
+ GiveTitanPassiveLifeLong( player, passive )
+ }
+ }
+
+ asset model = titan.GetModelName()
+ int skin = titan.GetSkin()
+ int camo = titan.GetCamo()
+ int decal = titan.GetDecal()
+ TitanSettings titanSettings = titan.ai.titanSettings
+ array<string> mods = titanSettings.titanSetFileMods
+
+ player.SetPlayerSettingsFromDataTable( { playerSetFile = titanSettings.titanSetFile, playerSetFileMods = mods } )
+ var title = GetPlayerSettingsFieldForClassName( settings, "printname" )
+
+ if ( title != null )
+ {
+ player.SetTitle( expect string( title ) )
+ }
+
+ if ( IsAlive( player ) )
+ TransferHealth( titan, player )
+
+ player.SetModel( model )
+ player.SetSkin( skin )
+ player.SetCamo( camo )
+ player.SetDecal( decal )
+ player.SetPoseParametersSameAs( titan )
+ player.SequenceTransitionFromEntity( titan )
+
+ // no cloak titan
+ player.SetCloakDuration( 0, 0, 0 )
+
+ // this must happen after changing the players settings
+ TransferDamageStates( titan, player )
+
+ titan.TransferTethersToEntity( player )
+
+ //We parent the player to the titan in the process of embarking
+ //Must clear parent when transfering children to avoid parenting the player to himself
+ if ( player.GetParent() == titan )
+ player.ClearParent()
+ //Transfer children after player has become titan model.
+ titan.TransferChildrenTo( player )
+
+ player.SetOrigin( titan.GetOrigin() )
+ player.SetAngles( titan.GetAngles() )
+ player.SetVelocity( Vector( 0,0,0 ) )
+
+ soul.e.embarkCount++
+ soul.Signal( "PlayerEmbarkedTitan", { player = player } )
+ player.Signal( "PlayerEmbarkedTitan", { titan = titan } )
+ titan.Signal( "TitanStopsThinking" )
+
+ // Added via AddCallback_OnPilotBecomesTitan
+ foreach ( callbackFunc in svGlobal.onPilotBecomesTitanCallbacks )
+ {
+ callbackFunc( player, titan )
+ }
+
+ #if DEV
+ thread Dev_CheckTitanIsDeletedAtEndOfPilotBecomesTitan( titan )
+ #endif
+}
+
+void function SetTitanSettings( TitanSettings titanSettings, string titanSetFile, array<string> mods = [] )
+{
+ Assert( titanSettings.titanSetFile == "", "Tried to set titan settings to " + titanSetFile + ", but its already set to " + titanSettings.titanSetFile )
+ titanSettings.titanSetFile = titanSetFile
+ titanSettings.titanSetFileMods = mods
+}
+
+void function Dev_CheckTitanIsDeletedAtEndOfPilotBecomesTitan( entity titan )
+{
+ WaitEndFrame()
+
+ Assert( !IsValid( titan ), "Titan should be deleted at end of PilotBecomesTitan" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_tonecontroller.nut b/Northstar.CustomServers/scripts/vscripts/mp/_tonecontroller.nut
new file mode 100644
index 000000000..786eda23c
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_tonecontroller.nut
@@ -0,0 +1,189 @@
+untyped
+
+global function ToneController_Init
+
+global function UpdateToneSettings
+
+global function SetAutoExposureMin
+global function SetAutoExposureMax
+global function SetAutoExposureCompensation
+global function SetAutoExposureCompensationBias
+global function SetAutoExposureRate
+global function UseDefaultAutoExposure
+global function SetBloomScale
+
+function ToneController_Init()
+{
+ level.toneController <- CreateEntity( "env_tonemap_controller" )
+ DispatchSpawn( level.toneController )
+
+ AddCallback_EntitiesDidLoad( UpdateToneSettings )
+}
+
+void function UpdateToneSettings()
+{
+ string mapName = GetMapName()
+
+ UseDefaultAutoExposure()
+
+ switch ( mapName )
+ {
+ case "mp_angel_city":
+ SetAutoExposureMin( 1.11 )
+ SetAutoExposureMax( 1.5 )
+ break;
+
+ case "mp_boneyard":
+ SetAutoExposureMin( 1.3 )
+ SetAutoExposureMax( 2.3 )
+ break;
+
+ case "mp_lagoon":
+ SetAutoExposureMin( 0.8 )
+ SetAutoExposureMax( 2.0 )
+ break;
+
+ case "mp_o2":
+ SetAutoExposureMin( 1.0 )
+ SetAutoExposureMax( 2.0 )
+ break;
+
+ case "mp_fracture":
+ SetAutoExposureMin( 1.25 )
+ SetAutoExposureMax( 4.0 )
+ break;
+
+ case "mp_training_ground":
+ SetAutoExposureMin( 0.6 )
+ SetAutoExposureMax( 2.5 )
+ break;
+
+ case "mp_relic":
+ SetAutoExposureMin( 0.9 )
+ SetAutoExposureMax( 2.0 )
+ break;
+
+ case "mp_smugglers_cove":
+ SetAutoExposureMin( 0.5 )
+ SetAutoExposureMax( 0.7 )
+ break;
+
+ case "mp_swampland":
+ SetAutoExposureMin( 0.5 )
+ SetAutoExposureMax( 0.8 )
+ break;
+
+ case "mp_runoff":
+ SetAutoExposureMin( 0.5 )
+ SetAutoExposureMax( 1.0 )
+ break;
+
+ case "mp_wargames":
+ SetAutoExposureMin( 1.0 )
+ SetAutoExposureMax( 1.75 )
+ break;
+
+ case "mp_harmony_mines":
+ SetAutoExposureMin( 1.0 )
+ SetAutoExposureMax( 1.75 )
+ break;
+
+ case "mp_switchback":
+ SetAutoExposureMin( 1.0 )
+ SetAutoExposureMax( 1.75 )
+ break;
+
+ case "mp_sandtrap":
+ SetAutoExposureMin( 0.5 )
+ SetAutoExposureMax( 1.15 )
+ break;
+
+ case "mp_taube_rock_photo_test":
+ SetAutoExposureMin( 1.2 )
+ SetAutoExposureMax( 2.0 )
+ SetBloomScale (1.0)
+ break;
+
+ case "mp_taube_forest_test":
+ SetAutoExposureMin( 1.2 )
+ SetAutoExposureMax( 2.0 )
+ SetBloomScale (1.0)
+ break;
+
+ case "mp_pbr_ball_test":
+ SetAutoExposureMin( 1.2 )
+ SetAutoExposureMax( 2.0 )
+ break;
+
+ case "mp_mendoko_taube_style":
+ SetBloomScale (1.0)
+ break;
+
+ case "mp_kodai_josh_style_01":
+ SetBloomScale (1.0)
+ break;
+
+ case "mp_fake_sky_taube_01":
+ SetBloomScale (1.0)
+ break;
+
+ case "sp_beacon_taube_style":
+ SetBloomScale (1.0)
+ break;
+
+ case "sp_trainer":
+ SetBloomScale( 0.2 )
+ SetAutoExposureMin( 0.8 )
+ SetAutoExposureMax( 0.8 )
+ break
+
+ case "sp_beacon":
+ SetAutoExposureMax( 5.0 )
+ break;
+
+ case "sp_beacon_spoke0":
+ SetAutoExposureMax( 5.0 )
+ break;
+
+ default:
+ UseDefaultAutoExposure()
+ break
+ }
+}
+
+
+
+function SetAutoExposureMin( float value )
+{
+ level.toneController.Fire( "SetAutoExposureMin", value )
+}
+
+function SetAutoExposureMax( float value )
+{
+ level.toneController.Fire( "SetAutoExposureMax", value )
+}
+
+function SetAutoExposureCompensation( float value )
+{
+ level.toneController.Fire( "SetAutoExposureCompensation", value )
+}
+
+function SetAutoExposureCompensationBias( float value )
+{
+ level.toneController.Fire( "SetAutoExposureCompensationBias", value )
+}
+
+function SetAutoExposureRate( float value )
+{
+ level.toneController.Fire( "SetAutoExposureRate", value )
+}
+
+function UseDefaultAutoExposure()
+{
+ level.toneController.Fire( "UseDefaultAutoExposure" )
+}
+
+function SetBloomScale( float value )
+{
+ level.toneController.Fire( "SetBloomScale", value )
+}
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_utility_mp.gnut b/Northstar.CustomServers/scripts/vscripts/mp/_utility_mp.gnut
new file mode 100644
index 000000000..ea7d9d447
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_utility_mp.gnut
@@ -0,0 +1,18 @@
+global function Utility_MP_Init
+global function ClientCommand_OnDevnetBugScreenshot
+global function SafeForTitanFall
+
+void function Utility_MP_Init()
+{
+
+}
+
+bool function ClientCommand_OnDevnetBugScreenshot( entity player, array<string> args )
+{
+ return true
+}
+
+bool function SafeForTitanFall( vector dropPoint )
+{
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/_vr.nut b/Northstar.CustomServers/scripts/vscripts/mp/_vr.nut
new file mode 100644
index 000000000..b9759ddf5
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/_vr.nut
@@ -0,0 +1,66 @@
+untyped
+
+global function VR_Init
+global function VR_GroundTroopsDeathCallback
+
+struct {
+ string vr_settings = ""
+} file
+
+function VR_Init( string settings = "", bool enableDropships = false )
+{
+ if ( reloadingScripts )
+ return
+
+ if ( !enableDropships )
+ FlagSet( "DisableDropships" )
+
+ file.vr_settings = settings
+
+ //AddDeathCallback( "npc_soldier", VR_GroundTroopsDeathCallback )
+ //AddDeathCallback( "npc_spectre", VR_GroundTroopsDeathCallback )
+ //AddDeathCallback( "npc_marvin", VR_GroundTroopsDeathCallback )
+ //AddDeathCallback( "player", VR_GroundTroopsDeathCallback )
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+}
+
+void function EntitiesDidLoad()
+{
+ if ( file.vr_settings.find( "no_evac" ) != null )
+ svGlobal.evacEnabled = false
+
+ if ( file.vr_settings.find( "no_npc" ) != null )
+ {
+ disable_npcs()
+ }
+
+ if ( file.vr_settings.find( "no_titan" ) != null )
+ {
+ Riff_ForceTitanAvailability( eTitanAvailability.Never )
+ FlagSet( "PilotBot" )
+ }
+}
+
+void function VR_GroundTroopsDeathCallback( entity guy, var damageInfo )
+{
+ EmitSoundAtPosition( TEAM_UNASSIGNED, guy.GetOrigin(), "Object_Dissolve" )
+
+ if ( ShouldDoDissolveDeath( guy, damageInfo ) )
+ guy.Dissolve( ENTITY_DISSOLVE_CHAR, Vector( 0, 0, 0 ), 0 )
+}
+
+function ShouldDoDissolveDeath( guy, damageInfo )
+{
+ if ( !guy.IsPlayer() )
+ return true
+
+ // can't dissolve players when they're not playing the game, otherwise when the game starts again they're invisible
+ local gs = GetGameState()
+ if ( gs != eGameState.Playing && gs != eGameState.SuddenDeath && gs != eGameState.Epilogue )
+ {
+ printt( "Skipping player dissolve death because game is not active ( player:", guy, ")" )
+ return false
+ }
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/_lf_maps_shared.gnut b/Northstar.CustomServers/scripts/vscripts/mp/levels/_lf_maps_shared.gnut
new file mode 100644
index 000000000..69ec56fb5
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/_lf_maps_shared.gnut
@@ -0,0 +1,8 @@
+global function SetupLiveFireMaps
+
+// live fire maps don't support alot of things like intros and titans, this makes sure those things are disabled
+void function SetupLiveFireMaps()
+{
+ Riff_ForceTitanAvailability( eTitanAvailability.Never )
+ ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, NOINTRO_INTRO_LENGTH )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_angel_city.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_angel_city.nut
new file mode 100644
index 000000000..3a5b637fa
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_angel_city.nut
@@ -0,0 +1,11 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ Evac_AddLocation( < 2527.889893, -2865.360107, 753.002991 >, < 0, -80.54, 0 > )
+ Evac_AddLocation( < 1253.530029, -554.075012, 811.125 >, < 0, 180, 0 > )
+ Evac_AddLocation( < 2446.989990, 809.364014, 576.0 >, < 0, 90.253, 0 > )
+ Evac_AddLocation( < -2027.430054, 960.395020, 609.007996 >, < 0, 179.604, 0 > )
+
+ Evac_SetSpacePosition( < -1700, -5500, -7600 >, < -3.620642, 270.307129, 0 > )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_angel_city_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_angel_city_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_angel_city_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_black_water_canal.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_black_water_canal.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_black_water_canal.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_black_water_canal_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_black_water_canal_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_black_water_canal_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_coliseum.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_coliseum.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_coliseum.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_coliseum_column.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_coliseum_column.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_coliseum_column.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_colony02.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_colony02.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_colony02.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_colony02_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_colony02_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_colony02_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_complex3.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_complex3.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_complex3.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_crashsite3.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_crashsite3.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_crashsite3.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_drydock.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_drydock.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_drydock.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_drydock_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_drydock_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_drydock_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_eden.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_eden.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_eden.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_forwardbase_kodai.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_forwardbase_kodai.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_forwardbase_kodai.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_forwardbase_kodai_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_forwardbase_kodai_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_forwardbase_kodai_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_glitch.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_glitch.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_glitch.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_glitch_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_glitch_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_glitch_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_grave.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_grave.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_grave.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_grave_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_grave_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_grave_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_homestead.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_homestead.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_homestead.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_homestead_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_homestead_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_homestead_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_deck.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_deck.nut
new file mode 100644
index 000000000..398b2fc55
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_deck.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_meadow.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_meadow.nut
new file mode 100644
index 000000000..398b2fc55
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_meadow.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_stacks.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_stacks.nut
new file mode 100644
index 000000000..398b2fc55
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_stacks.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_township.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_township.nut
new file mode 100644
index 000000000..398b2fc55
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_township.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_traffic.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_traffic.nut
new file mode 100644
index 000000000..398b2fc55
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_traffic.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_uma.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_uma.nut
new file mode 100644
index 000000000..398b2fc55
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_lf_uma.nut
@@ -0,0 +1,6 @@
+global function CodeCallback_MapInit
+
+void function CodeCallback_MapInit()
+{
+ SetupLiveFireMaps()
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_relic02.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_relic02.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_relic02.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_relic02_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_relic02_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_relic02_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_rise.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_rise.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_rise.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_rise_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_rise_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_rise_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_thaw.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_thaw.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_thaw.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_thaw_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_thaw_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_thaw_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_wargames.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_wargames.nut
new file mode 100644
index 000000000..9252cced5
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_wargames.nut
@@ -0,0 +1,415 @@
+untyped
+
+const POD_ATTACHPOINT = "REF"
+
+const FX_LIGHT_ORANGE = "runway_light_orange"
+const FX_LIGHT_GREEN = "runway_light_green"
+const FX_POD_LASER = "P_pod_scan_laser_FP"
+const FX_POD_GLOWLIGHT = "P_pod_door_glow_FP"
+const FX_POD_SCREEN_IN = "P_pod_screen_lasers_IN"
+const FX_POD_SCREEN_OUT = "P_pod_screen_lasers_OUT"
+const FX_POD_DLIGHT_CONSOLE1 = $"P_pod_Dlight_console1"
+const FX_POD_DLIGHT_CONSOLE2 = $"P_pod_Dlight_console2"
+const FX_POD_DLIGHT_BACKLIGHT_SIDE = "P_pod_Dlight_backlight_side"
+const FX_POD_DLIGHT_BACKLIGHT_TOP = "P_pod_Dlight_backlight_top"
+const FX_TITAN_COCKPIT_LIGHT = "xo_cockpit_dlight"
+
+struct TrainingPod_dLightMapping
+{
+ string scriptAlias
+ asset fxName
+ string attachName
+ entity fxHandle
+}
+
+struct TrainingPod_LaserEmitter
+{
+ entity ent
+ string attachName
+ vector ogAng
+ bool sweepDone = false
+ entity fxHandle
+}
+
+struct TrainingPod_GlowLightRow
+{
+ array<string> fxSpotsL
+ array<string> fxSpotsR
+}
+
+global function CodeCallback_MapInit
+global function WargamesIntroStart
+
+struct {
+ entity militiaTrainingPod
+ entity imcTrainingPod
+ array< TrainingPod_dLightMapping >[2] podDLightMappings
+ array< TrainingPod_LaserEmitter >[2] podLaserEmitters
+
+ array< entity > playersWatchingIntro
+ string currentPodAnim
+ float currentPodAnimStartTime
+} file
+
+void function CodeCallback_MapInit()
+{
+ //FlagInit( "TrainingPodSetupDone" )
+ //FlagInit( "IntroRunning" )
+ //
+ //FlagSet( "DogFights" )
+ //FlagSet( "StratonFlybys" )
+ //
+ //ClassicMP_SetIntroLevelSetupFunc( Wargames_Intro_LevelSetupFunc )
+ //ClassicMP_SetIntroPlayerSpawnFunc( Wargames_ClassicMPIntroSpawn )
+ //ClassicMP_SetPrematchSpawnPlayersFunc( Wargames_PrematchSpawnPlayersFunc )
+ //
+ //AddCallback_GameStateEnter( eGameState.WaitingForPlayers, WargamesIntroStart ) // This starts the main intro control thread
+ //AddCallback_GameStateEnter( eGameState.PickLoadout, Wargames_GameStateEnterFunc_PickLoadout )
+ //AddCallback_GameStateEnter( eGameState.Prematch, Wargames_GameStateEnterFunc_PrematchCallback )
+}
+
+// setup
+
+bool function Wargames_Intro_LevelSetupFunc()
+{
+ array< entity > militiaTrainingPods = GetEntArrayByName_Expensive( "training_pod" )
+ Assert( militiaTrainingPods.len() == 1 )
+ file.militiaTrainingPod = militiaTrainingPods[0]
+ file.militiaTrainingPod.s.teamIdx <- TEAM_MILITIA
+
+ array< entity > imcTrainingPods = GetEntArrayByName_Expensive( "training_pod_imc" )
+ Assert( imcTrainingPods.len() == 1 )
+ file.imcTrainingPod = imcTrainingPods[0]
+ file.imcTrainingPod.s.teamIdx <- TEAM_IMC
+
+ SetupTrainingPod( file.militiaTrainingPod )
+ SetupTrainingPod( file.imcTrainingPod )
+
+ FlagSet( "TrainingPodSetupDone" )
+ return true
+}
+
+void function SetupTrainingPod( entity pod )
+{
+ pod.DisableHibernation()
+ pod.s.glowLightFXHandles <- []
+ pod.s.dLights <- []
+
+ // FUN hack because can't assign struct => var
+ int podStructIndex = pod.s.teamIdx == TEAM_MILITIA ? 0 : 1
+
+ array< TrainingPod_dLightMapping > tempLightMappings
+
+ TrainingPod_dLightMapping m1
+ m1.scriptAlias = "console1"
+ m1.fxName = FX_POD_DLIGHT_CONSOLE1
+ m1.attachName = "light_console1"
+ tempLightMappings.append( m1 )
+
+ TrainingPod_dLightMapping m2
+ m2.scriptAlias = "console2"
+ m2.fxName = FX_POD_DLIGHT_CONSOLE2
+ m2.attachName = "light_console2"
+ tempLightMappings.append( m2 )
+
+ file.podDLightMappings[ podStructIndex ] = tempLightMappings
+
+ array< string > laserAttachNames = [ "fx_laser_L", "fx_laser_R" ]
+
+ foreach ( string attachName in laserAttachNames )
+ {
+ entity emitterEnt = CreateScriptMover( pod.GetOrigin(), pod.GetAngles() )
+ int attachID = pod.LookupAttachment( attachName )
+ vector attachOrg = pod.GetAttachmentOrigin( attachID )
+ vector attachAng = pod.GetAttachmentAngles( attachID )
+
+ TrainingPod_LaserEmitter emitter
+ emitter.ent = emitterEnt
+ emitter.attachName = attachName
+ emitter.ogAng = attachAng
+
+ file.podLaserEmitters[ podStructIndex ].append( emitter )
+ }
+
+ // HACK we do this later as well to reset the emitter positions, so it's a separate function
+ TrainingPod_SnapLaserEmittersToAttachPoints( pod )
+}
+
+void function TrainingPod_SnapLaserEmittersToAttachPoints( entity pod )
+{
+ foreach ( TrainingPod_LaserEmitter emitter in file.podLaserEmitters[ pod.s.teamIdx == TEAM_MILITIA ? 0 : 1 ] )
+ {
+ int attachID = pod.LookupAttachment( emitter.attachName )
+ vector attachOrg = pod.GetAttachmentOrigin( attachID )
+ vector attachAng = pod.GetAttachmentAngles( attachID )
+
+ emitter.ent.ClearParent()
+ emitter.ent.SetOrigin( attachOrg ) // HACK set this to ANYTHING (even 0, 0, 0) and the position is correct, otherwise it's offset from the attachpoint when parented
+ emitter.ent.SetParent( pod, emitter.attachName )
+ }
+}
+
+// spawning/intro sequence
+
+void function Wargames_ClassicMPIntroSpawn( entity player )
+{
+ Assert( !IsAlive( player ) )
+
+ thread PlayerWatchPodIntro( player )
+ // return true
+}
+
+void function Wargames_PrematchSpawnPlayersFunc( array< entity > players )
+{
+
+}
+
+void function PlayerWatchPodIntro( entity player )
+{
+ player.EndSignal( "Disconnected" )
+ player.EndSignal( "OnDeath" )
+
+ int team = player.GetTeam()
+
+ entity pod
+ if ( team == TEAM_MILITIA )
+ pod = file.militiaTrainingPod
+ else
+ pod = file.imcTrainingPod
+
+ AddCinematicFlag( player, CE_FLAG_INTRO )
+ file.playersWatchingIntro.append( player )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ {
+ printt( "PlayerWatchPodIntro: clearing up player:", player )
+
+ //ArrayRemove( file.playersWatchingIntro, player )
+ file.playersWatchingIntro.remove( file.playersWatchingIntro.find( player ) )
+
+ player.Anim_Stop()
+ //player.ClearAnimViewEntity()
+ //player.ClearParent()
+ player.UnforceStand()
+ player.EnableWeaponViewModel()
+ player.kv.VisibilityFlags = 7 // All can see now that intro is over
+
+ FadeOutSoundOnEntity( player, "Amb_Wargames_Pod_Ambience", 0.13 )
+
+ // turns hud back on
+ if ( HasCinematicFlag( player, CE_FLAG_INTRO ) )
+ RemoveCinematicFlag( player, CE_FLAG_INTRO )
+ }
+ }
+ )
+
+ // FIRST SPAWN
+ int attachID = pod.LookupAttachment( POD_ATTACHPOINT )
+ vector podRefOrg = pod.GetAttachmentOrigin( attachID )
+ vector podRefAng = pod.GetAttachmentAngles( attachID )
+ player.SetOrigin( podRefOrg )
+ player.SetAngles( podRefAng )
+ player.RespawnPlayer( null )
+
+ player.kv.VisibilityFlags = 1 // visible to player only, so others don't see his viewmodel during the anim
+ player.DisableWeaponViewModel()
+ player.ForceStand()
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Amb_Wargames_Pod_Ambience" )
+ thread Intro_StartPlayerFirstPersonSequence( player )
+
+ FlagWait( "IntroRunning" )
+ player.UnfreezeControlsOnServer()
+
+ // wait for intro to finish for OnThreadEnd cleanup
+ FlagWaitClear( "IntroRunning" )
+ printt( "PlayerWatchPodIntro: intro finished normally for player", player )
+}
+
+void function Intro_StartPlayerFirstPersonSequence( entity player )
+{
+ player.EndSignal( "Disconnected" )
+ player.EndSignal( "OnDeath" )
+
+ entity pod
+ if ( player.GetTeam() == TEAM_MILITIA )
+ pod = file.militiaTrainingPod
+ else
+ pod = file.imcTrainingPod
+
+ FirstPersonSequenceStruct playerSequence
+ playerSequence.blendTime = 0.0
+ playerSequence.attachment = POD_ATTACHPOINT
+ playerSequence.renderWithViewModels = true
+ playerSequence.firstPersonAnimIdle = "ptpov_trainingpod_idle"
+
+ //FirstPersonSequenceStruct podSequence
+ //podSequence.blendTime = 0.25
+ //podSequence.thirdPersonAnim = "trainingpod_doors_close"
+ //podSequence.thirdPersonAnimIdle = "trainingpod_doors_close_idle"
+ //podSequence.renderWithViewModels = true
+
+ void functionref( entity ) viewconeFunc = null
+ viewconeFunc = TrainingPod_ViewConeLock_SemiStrict
+
+ // i don't get this whole animevent system tbh, might just skip it lol
+ entity viewmodel = player.GetFirstPersonProxy()
+ //if ( !HasAnimEvent( viewmodel, "PlaySound_SimPod_DoorShut" ) )
+ //{
+ printt( "adding anim event... this probably won't work", player )
+ AddAnimEvent( viewmodel, "PlaySound_SimPod_DoorShut", PlaySound_SimPod_DoorShut )
+ AddAnimEvent( viewmodel, "PlaySound_SimPod_DoorShut", PlaySound_SimPod_DoorShut )
+ AddAnimEvent( viewmodel, "PlaySound_SimPod_DoorShut", PlaySound_SimPod_DoorShut )
+ AddAnimEvent( viewmodel, "PlaySound_SimPod_DoorShut", PlaySound_SimPod_DoorShut )
+ AddAnimEvent( viewmodel, "PlaySound_SimPod_DoorShut", PlaySound_SimPod_DoorShut )
+
+ print( "this SHOULD have caused errors in console due to registering it multiple times but doesn't????" )
+ //}
+
+ if ( file.currentPodAnim == "closing" )
+ {
+ playerSequence.firstPersonAnim = "ptpov_trainingpod_doors_close"
+ }
+
+ // TODO: correct viewcone funcs and catchup stuff
+
+ //thread FirstPersonSequence( podSequence, pod )
+ thread FirstPersonSequence( playerSequence, player, pod )
+
+ if ( viewconeFunc != null )
+ viewconeFunc( player )
+
+ FlagClear( "IntroRunning" )
+}
+
+void function Intro_StartPlayerFirstPersonSequenceForAll()
+{
+ foreach ( entity player in file.playersWatchingIntro )
+ thread Intro_StartPlayerFirstPersonSequence( player )
+}
+
+void function TrainingPod_ViewConeLock_SemiStrict( entity player )
+{
+ player.PlayerCone_FromAnim()
+ player.PlayerCone_SetMinYaw( -10 )
+ player.PlayerCone_SetMaxYaw( 10 )
+ player.PlayerCone_SetMinPitch( -10 )
+ player.PlayerCone_SetMaxPitch( 10 )
+}
+
+void function PlaySound_SimPod_DoorShut( entity playerFirstPersonProxy )
+{
+ print( "playing anim event!" )
+ // never called lol
+
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "NPE_Scr_SimPod_DoorShut" )
+}
+
+void function WargamesIntroStart()
+{
+ thread WargamesIntroStartThreaded()
+}
+
+void function WargamesIntroStartThreaded()
+{
+ FlagWait( "TrainingPodSetupDone" )
+
+ // early connecting players start idling
+ file.currentPodAnim = "idle"
+
+ file.militiaTrainingPod.s.ambienceLoud <- "Wargames_Emit_MCOR_Intro_HighPass"
+ file.militiaTrainingPod.s.ambienceQuiet <- "Wargames_Emit_MCOR_Intro_LowPass"
+
+ file.imcTrainingPod.s.ambienceLoud <- "Wargames_Emit_IMC_Intro_HighPass"
+ file.imcTrainingPod.s.ambienceQuiet <- "Wargames_Emit_IMC_Intro_LowPass"
+
+ OnThreadEnd(
+ function() : ()
+ {
+
+ }
+ )
+
+ // wait for prematch, after waiting for players
+ WaittillGameStateOrHigher( eGameState.Prematch )
+
+ EmitSoundOnEntity( file.militiaTrainingPod, "Wargames_Emit_MCOR_Intro_HighPass" )
+ EmitSoundOnEntity( file.militiaTrainingPod, "Wargames_Emit_MCOR_Intro_LowPass" )
+
+ EmitSoundOnEntity( file.imcTrainingPod, "Wargames_Emit_IMC_Intro_HighPass")
+ EmitSoundOnEntity( file.imcTrainingPod, "Wargames_Emit_IMC_Intro_LowPass" )
+
+ // todo: intro skits
+
+ FlagSet( "IntroRunning" )
+ float startTime = Time()
+
+ file.currentPodAnimStartTime = Time()
+ FirstPersonSequenceStruct podSequence
+ podSequence.blendTime = 0.0
+ podSequence.thirdPersonAnimIdle = "trainingpod_doors_open_idle"
+ podSequence.renderWithViewModels = true
+
+ // turn on top lights
+ TrainingPod_TurnOnInteriorDLight( "console1", file.militiaTrainingPod )
+ TrainingPod_TurnOnInteriorDLight( "console2", file.militiaTrainingPod )
+ thread FirstPersonSequence( podSequence, file.militiaTrainingPod )
+
+ TrainingPod_TurnOnInteriorDLight( "console1", file.imcTrainingPod )
+ TrainingPod_TurnOnInteriorDLight( "console2", file.imcTrainingPod )
+ thread FirstPersonSequence( podSequence, file.imcTrainingPod )
+
+ //thread Intro_PlayersHearPodVO()
+
+ Intro_StartPlayerFirstPersonSequenceForAll()
+ wait 8
+
+ file.currentPodAnim = "closing"
+ file.currentPodAnimStartTime = Time()
+ podSequence.blendTime = 0.0
+ podSequence.thirdPersonAnim = "trainingpod_doors_close"
+ podSequence.thirdPersonAnimIdle = "trainingpod_doors_close_idle"
+
+ Intro_StartPlayerFirstPersonSequenceForAll()
+
+ thread FirstPersonSequence( podSequence, file.militiaTrainingPod )
+ waitthread FirstPersonSequence( podSequence, file.imcTrainingPod )
+
+ file.currentPodAnim = "done"
+
+ wait 4.5
+
+
+ FlagClear( "IntroRunning" )
+}
+
+void function TrainingPod_TurnOnInteriorDLight( string alias, entity pod )
+{
+ int teamIdx = pod.s.teamIdx == TEAM_MILITIA ? 0 : 1
+ foreach ( TrainingPod_dLightMapping mapping in file.podDLightMappings[ teamIdx ] )
+ {
+ if ( mapping.scriptAlias == alias )
+ {
+ PlayLoopFXOnEntity( mapping.fxName, pod, mapping.attachName )
+ pod.s.dLights.append( mapping.fxName )
+ break
+ }
+ }
+}
+
+void function Wargames_GameStateEnterFunc_PickLoadout()
+{
+ level.nv.minPickLoadOutTime = Time() + 0.5
+}
+
+void function Wargames_GameStateEnterFunc_PrematchCallback()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_wargames_fd.nut b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_wargames_fd.nut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/levels/mp_wargames_fd.nut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/pintelemetry.gnut b/Northstar.CustomServers/scripts/vscripts/mp/pintelemetry.gnut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/pintelemetry.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/player_cloak.nut b/Northstar.CustomServers/scripts/vscripts/mp/player_cloak.nut
new file mode 100644
index 000000000..8ef7dcd93
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/player_cloak.nut
@@ -0,0 +1,184 @@
+untyped //TODO: get rid of player.s.cloakedShotsAllowed. (Referenced in base_gametype_sp, so remove for R5)
+
+global function PlayerCloak_Init
+
+global const CLOAK_FADE_IN = 1.0
+global const CLOAK_FADE_OUT = 1.0
+
+global function EnableCloak
+global function DisableCloak
+global function EnableCloakForever
+global function DisableCloakForever
+
+//=========================================================
+// player_cloak
+//
+//=========================================================
+
+void function PlayerCloak_Init()
+{
+ RegisterSignal( "OnStartCloak" )
+ RegisterSignal( "KillHandleCloakEnd" ) //Somewhat awkward, mainly to smooth out weird interactions with cloak ability and cloak execution
+
+ AddCallback_OnPlayerKilled( AbilityCloak_OnDeath )
+ AddSpawnCallback( "npc_titan", SetCannotCloak )
+}
+
+void function SetCannotCloak( entity ent )
+{
+ ent.SetCanCloak( false )
+}
+
+void function PlayCloakSounds( entity player )
+{
+ EmitSoundOnEntityOnlyToPlayer( player, player, "cloak_on_1P" )
+ EmitSoundOnEntityExceptToPlayer( player, player, "cloak_on_3P" )
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "cloak_sustain_loop_1P" )
+ EmitSoundOnEntityExceptToPlayer( player, player, "cloak_sustain_loop_3P" )
+}
+
+void function EnableCloak( entity player, float duration, float fadeIn = CLOAK_FADE_IN )
+{
+ if ( player.cloakedForever )
+ return
+
+ thread AICalloutCloak( player )
+
+ PlayCloakSounds( player )
+
+ float cloakDuration = duration - fadeIn
+
+ Assert( cloakDuration > 0.0, "Not valid cloak duration. Check that duration is larger than the fadeinTime. When this is not true it will cause the player to be cloaked forever. If you want to do that use EnableCloakForever instead" )
+
+ player.SetCloakDuration( fadeIn, cloakDuration, CLOAK_FADE_OUT )
+
+ player.s.cloakedShotsAllowed = 0
+
+ Battery_StopFXAndHideIconForPlayer( player )
+
+ thread HandleCloakEnd( player )
+}
+
+void function AICalloutCloak( entity player )
+{
+ player.EndSignal( "OnDeath" )
+
+ wait CLOAK_FADE_IN //Give it a beat after cloak has finishing cloaking in
+
+ array<entity> nearbySoldiers = GetNPCArrayEx( "npc_soldier", TEAM_ANY, player.GetTeam(), player.GetOrigin(), 1000 ) //-1 for distance parameter means all spectres in map
+ foreach ( entity grunt in nearbySoldiers )
+ {
+ if ( !IsAlive( grunt ) )
+ continue
+
+ if ( grunt.GetEnemy() == player )
+ {
+ ScriptDialog_PilotCloaked( grunt, player )
+ return //Only need one guy to say this instead of multiple guys
+ }
+ }
+}
+
+void function EnableCloakForever( entity player )
+{
+ player.SetCloakDuration( CLOAK_FADE_IN, -1, CLOAK_FADE_OUT )
+
+ player.cloakedForever = true
+
+ thread HandleCloakEnd( player )
+ PlayCloakSounds( player )
+}
+
+
+void function DisableCloak( entity player, float fadeOut = CLOAK_FADE_OUT )
+{
+ StopSoundOnEntity( player, "cloak_sustain_loop_1P" )
+ StopSoundOnEntity( player, "cloak_sustain_loop_3P" )
+
+ bool wasCloaked = player.IsCloaked( CLOAK_INCLUDE_FADE_IN_TIME )
+
+ if ( fadeOut < CLOAK_FADE_OUT && wasCloaked )
+ {
+ EmitSoundOnEntityOnlyToPlayer( player, player, "cloak_interruptend_1P" )
+ EmitSoundOnEntityExceptToPlayer( player, player, "cloak_interruptend_3P" )
+
+ StopSoundOnEntity( player, "cloak_warningtoend_1P" )
+ StopSoundOnEntity( player, "cloak_warningtoend_3P" )
+ }
+
+ player.SetCloakDuration( 0, 0, fadeOut )
+}
+
+void function DisableCloakForever( entity player, float fadeOut = CLOAK_FADE_OUT )
+{
+ DisableCloak( player, fadeOut )
+ player.cloakedForever = false
+}
+
+
+void function HandleCloakEnd( entity player )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnEMPPilotHit" )
+ player.EndSignal( "OnChangedPlayerClass" )
+ player.Signal( "OnStartCloak" )
+ player.EndSignal( "OnStartCloak" )
+ player.EndSignal( "KillHandleCloakEnd" ) //Calling DisableCloak() after EnableCloak() doesn't kill this thread by design (to allow attacking through cloak etc), so this signal is for when you want to kill this thread
+
+ float duration = player.GetCloakEndTime() - Time()
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( !IsValid( player ) )
+ return
+
+ if ( PlayerHasBattery( player ) )
+ Battery_StartFX( GetBatteryOnBack( player ) )
+
+ StopSoundOnEntity( player, "cloak_sustain_loop_1P" )
+ StopSoundOnEntity( player, "cloak_sustain_loop_3P" )
+ if ( !IsCloaked( player ) )
+ return
+
+ if ( !IsAlive( player ) || !player.IsHuman() )
+ {
+ DisableCloak( player )
+ return
+ }
+
+ float duration = player.GetCloakEndTime() - Time()
+ if ( duration <= 0 )
+ {
+ DisableCloak( player )
+ }
+ }
+ )
+
+ float soundBufferTime = 3.45
+
+ if ( duration > soundBufferTime )
+ {
+ wait ( duration - soundBufferTime )
+ if ( !IsCloaked( player ) )
+ return
+ EmitSoundOnEntityOnlyToPlayer( player, player, "cloak_warningtoend_1P" )
+ EmitSoundOnEntityExceptToPlayer( player, player, "cloak_warningtoend_3P" )
+
+ wait soundBufferTime
+ }
+ else
+ {
+ wait duration
+ }
+}
+
+
+void function AbilityCloak_OnDeath( entity player, entity attacker, var damageInfo )
+{
+ if ( !IsCloaked( player ) )
+ return
+
+ DisableCloak( player, 0 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/spawn.nut b/Northstar.CustomServers/scripts/vscripts/mp/spawn.nut
new file mode 100644
index 000000000..b7a50453e
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/spawn.nut
@@ -0,0 +1,428 @@
+untyped
+
+global function InitRatings // temp for testing
+
+global function Spawn_Init
+global function SetSpawnsUseFrontline
+global function SetRespawnsEnabled
+global function RespawnsEnabled
+global function SetSpawnpointGamemodeOverride
+global function CreateNoSpawnArea
+global function DeleteNoSpawnArea
+
+global function GetCurrentFrontline
+global function FindSpawnPoint
+
+global function RateSpawnpoints_Generic
+
+struct NoSpawnArea
+{
+ string id
+ int blockedTeam
+ int blockOtherTeams
+ vector position
+ float lifetime
+ float radius
+}
+
+struct {
+ bool respawnsEnabled = true
+ string spawnpointGamemodeOverride
+
+ array<vector> preferSpawnNodes
+ table<string, NoSpawnArea> noSpawnAreas
+ bool sidesSwitched = false
+
+ bool frontlineBased = false
+ float lastImcFrontlineRatingTime
+ float lastMilitiaFrontlineRatingTime
+ Frontline& lastImcFrontline
+ Frontline& lastMilitiaFrontline
+} file
+
+void function Spawn_Init()
+{
+ AddCallback_GameStateEnter( eGameState.SwitchingSides, OnSwitchingSides )
+ AddCallback_EntitiesDidLoad( InitPreferSpawnNodes )
+
+ AddSpawnCallback( "info_spawnpoint_human", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_human_start", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_titan", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_titan_start", InitSpawnpoint )
+}
+
+void function InitPreferSpawnNodes()
+{
+ foreach ( entity hardpoint in GetEntArrayByClass_Expensive( "info_hardpoint" ) )
+ {
+ if ( !hardpoint.HasKey( "hardpointGroup" ) )
+ continue
+
+ if ( hardpoint.kv.hardpointGroup != "A" && hardpoint.kv.hardpointGroup != "B" && hardpoint.kv.hardpointGroup != "C" )
+ continue
+
+ file.preferSpawnNodes.append( hardpoint.GetOrigin() )
+ }
+
+ //foreach ( entity frontline in GetEntArrayByClass_Expensive( "info_frontline" ) )
+ // file.preferSpawnNodes.append( frontline.GetOrigin() )
+}
+
+void function InitSpawnpoint( entity spawnpoint )
+{
+ spawnpoint.s.lastUsedTime <- -999
+}
+
+void function SetRespawnsEnabled( bool enabled )
+{
+ file.respawnsEnabled = enabled
+}
+
+bool function RespawnsEnabled()
+{
+ return file.respawnsEnabled
+}
+
+string function CreateNoSpawnArea( int blockSpecificTeam, int blockEnemiesOfTeam, vector position, float lifetime, float radius )
+{
+ NoSpawnArea noSpawnArea
+ noSpawnArea.blockedTeam = blockSpecificTeam
+ noSpawnArea.blockOtherTeams = blockEnemiesOfTeam
+ noSpawnArea.position = position
+ noSpawnArea.lifetime = lifetime
+ noSpawnArea.radius = radius
+
+ // generate an id
+ noSpawnArea.id = UniqueString( "noSpawnArea" )
+
+ thread NoSpawnAreaLifetime( noSpawnArea )
+
+ return noSpawnArea.id
+}
+
+void function NoSpawnAreaLifetime( NoSpawnArea noSpawnArea )
+{
+ wait noSpawnArea.lifetime
+ DeleteNoSpawnArea( noSpawnArea.id )
+}
+
+void function DeleteNoSpawnArea( string noSpawnIdx )
+{
+ try // unsure if the trycatch is necessary but better safe than sorry
+ {
+ delete file.noSpawnAreas[ noSpawnIdx ]
+ }
+ catch ( exception )
+ {}
+}
+
+void function SetSpawnpointGamemodeOverride( string gamemode )
+{
+ file.spawnpointGamemodeOverride = gamemode
+}
+
+void function SetSpawnsUseFrontline( bool useFrontline )
+{
+ file.frontlineBased = useFrontline
+}
+
+bool function InitRatings( entity player, int team )
+{
+ Frontline frontline = GetCurrentFrontline( team )
+ print( team )
+ print( frontline.friendlyCenter )
+
+ vector offsetOrigin = frontline.friendlyCenter + frontline.combatDir * 256
+ SpawnPoints_InitFrontlineData( offsetOrigin, frontline.combatDir, frontline.line, frontline.friendlyCenter, 2.0 ) // temp
+
+ if ( player != null )
+ SpawnPoints_InitRatings( player, team ) // no idea what the second arg supposed to be lol
+
+ return frontline.friendlyCenter == < 0, 0, 0 > && file.frontlineBased // if true, use startspawns
+}
+
+Frontline function GetCurrentFrontline( int team )
+{
+ float lastFrontlineRatingTime
+ Frontline lastFrontline
+ if ( team == TEAM_IMC )
+ {
+ lastFrontline = file.lastImcFrontline
+ lastFrontlineRatingTime = file.lastImcFrontlineRatingTime
+ }
+ else
+ {
+ lastFrontline = file.lastMilitiaFrontline
+ lastFrontlineRatingTime = file.lastMilitiaFrontlineRatingTime
+ }
+
+ // current frontline is old, get a new one
+ if ( lastFrontlineRatingTime + 20.0 < Time() || lastFrontline.friendlyCenter == < 0, 0, 0 > )
+ {
+ print( "rerating frontline..." )
+ Frontline frontline = GetFrontline( team )
+
+ // this doesn't work lol
+ /*if ( frontline.friendlyCenter == < 0, 0, 0 > )
+ {
+ // recalculate to startspawnpoint positions
+ array<entity> startSpawns = SpawnPoints_GetPilotStart( team )
+
+ vector averagePos
+ vector averageDir
+ foreach ( entity spawnpoint in startSpawns )
+ {
+ averagePos.x += spawnpoint.GetOrigin().x
+ averagePos.y += spawnpoint.GetOrigin().y
+ averagePos.z += spawnpoint.GetOrigin().z
+
+ averageDir.x += spawnpoint.GetAngles().x
+ averageDir.y += spawnpoint.GetAngles().y
+ averageDir.z += spawnpoint.GetAngles().z
+ }
+
+ averagePos.x /= startSpawns.len()
+ averagePos.y /= startSpawns.len()
+ averagePos.z /= startSpawns.len()
+
+ averageDir.x /= startSpawns.len()
+ averageDir.y /= startSpawns.len()
+ averageDir.z /= startSpawns.len()
+
+ print( "average " + averagePos )
+
+ frontline.friendlyCenter = averagePos
+ frontline.origin = averagePos
+ frontline.combatDir = averageDir * -1
+ }*/
+
+ if ( team == TEAM_IMC )
+ {
+ file.lastImcFrontlineRatingTime = Time()
+ file.lastImcFrontline = frontline
+ }
+ else
+ {
+ file.lastMilitiaFrontlineRatingTime = Time()
+ file.lastMilitiaFrontline = frontline
+ }
+
+ lastFrontline = frontline
+ }
+
+ return lastFrontline
+}
+
+entity function FindSpawnPoint( entity player, bool isTitan, bool useStartSpawnpoint )
+{
+ int team = player.GetTeam()
+ if ( file.sidesSwitched )
+ team = GetOtherTeam( team )
+
+ useStartSpawnpoint = InitRatings( player, player.GetTeam() ) || useStartSpawnpoint // force startspawns if no frontline
+ print( "useStartSpawnpoint: " + useStartSpawnpoint )
+
+ array<entity> spawnpoints
+ if ( useStartSpawnpoint )
+ spawnpoints = isTitan ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetPilotStart( team )
+ else
+ spawnpoints = isTitan ? SpawnPoints_GetTitan() : SpawnPoints_GetPilot()
+
+ void functionref( int, array<entity>, int, entity ) ratingFunc = isTitan ? GameMode_GetTitanSpawnpointsRatingFunc( GAMETYPE ) : GameMode_GetPilotSpawnpointsRatingFunc( GAMETYPE )
+ ratingFunc( isTitan ? TD_TITAN : TD_PILOT, spawnpoints, team, player )
+
+ if ( isTitan )
+ {
+ if ( useStartSpawnpoint )
+ SpawnPoints_SortTitanStart()
+ else
+ SpawnPoints_SortTitan()
+
+ spawnpoints = useStartSpawnpoint ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetTitan()
+ }
+ else
+ {
+ if ( useStartSpawnpoint )
+ SpawnPoints_SortPilotStart()
+ else
+ SpawnPoints_SortPilot()
+
+ spawnpoints = useStartSpawnpoint ? SpawnPoints_GetPilotStart( team ) : SpawnPoints_GetPilot()
+ }
+
+ entity spawnpoint = GetBestSpawnpoint( player, spawnpoints )
+
+ spawnpoint.s.lastUsedTime = Time()
+ player.SetLastSpawnPoint( spawnpoint )
+
+ return spawnpoint
+}
+
+entity function GetBestSpawnpoint( entity player, array<entity> spawnpoints )
+{
+ // not really 100% sure on this randomisation, needs some thought
+ array<entity> validSpawns
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ if ( IsSpawnpointValid( spawnpoint, player.GetTeam() ) )
+ {
+ validSpawns.append( spawnpoint )
+
+ if ( validSpawns.len() == 3 ) // arbitrary small sample size
+ break
+ }
+ }
+
+ if ( validSpawns.len() == 0 )
+ {
+ // no valid spawns, very bad, so dont care about spawns being valid anymore
+ print( "found no valid spawns! spawns may be subpar!" )
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ validSpawns.append( spawnpoint )
+
+ if ( validSpawns.len() == 3 ) // arbitrary small sample size
+ break
+ }
+ }
+
+ return validSpawns[ RandomInt( validSpawns.len() ) ] // slightly randomize it
+}
+
+bool function IsSpawnpointValid( entity spawnpoint, int team )
+{
+ //if ( !spawnpoint.HasKey( "ignoreGamemode" ) || ( spawnpoint.HasKey( "ignoreGamemode" ) && spawnpoint.kv.ignoreGamemode == "0" ) ) // used by script-spawned spawnpoints
+ //{
+ // if ( file.spawnpointGamemodeOverride != "" )
+ // {
+ // string gamemodeKey = "gamemode_" + file.spawnpointGamemodeOverride
+ // if ( spawnpoint.HasKey( gamemodeKey ) && ( spawnpoint.kv[ gamemodeKey ] == "0" || spawnpoint.kv[ gamemodeKey ] == "" ) )
+ // return false
+ // }
+ // else if ( GameModeRemove( spawnpoint ) )
+ // return false
+ //}
+
+ if ( Riff_FloorIsLava() && spawnpoint.GetOrigin().z < GetLethalFogTop() )
+ return false
+
+ int compareTeam = spawnpoint.GetTeam()
+ if ( file.sidesSwitched && ( compareTeam == TEAM_MILITIA || compareTeam == TEAM_IMC ) )
+ compareTeam = GetOtherTeam( compareTeam )
+
+ if ( spawnpoint.GetTeam() > 0 && compareTeam != team && !IsFFAGame() )
+ return false
+
+ if ( spawnpoint.IsOccupied() )
+ return false
+
+ foreach ( k, NoSpawnArea noSpawnArea in file.noSpawnAreas )
+ {
+ if ( Distance( noSpawnArea.position, spawnpoint.GetOrigin() ) > noSpawnArea.radius )
+ continue
+
+ if ( noSpawnArea.blockedTeam != TEAM_INVALID && noSpawnArea.blockedTeam == team )
+ return false
+
+ if ( noSpawnArea.blockOtherTeams != TEAM_INVALID && noSpawnArea.blockOtherTeams != team )
+ return false
+ }
+
+ array<entity> projectiles = GetProjectileArrayEx( "any", TEAM_ANY, TEAM_ANY, spawnpoint.GetOrigin(), 400 )
+ foreach ( entity projectile in projectiles )
+ if ( projectile.GetTeam() != team )
+ return false
+
+ if ( Time() - spawnpoint.s.lastUsedTime <= 1.0 )
+ return false
+
+ return true
+}
+
+void function RateSpawnpoints_Generic( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ // calculate ratings for preferred nodes
+ // this tries to prefer nodes with more teammates, then activity on them
+ // todo: in the future it might be good to have this prefer nodes with enemies up to a limit of some sort
+ // especially in ffa modes i could deffo see this falling apart a bit rn
+ // perhaps dead players could be used to calculate some sort of activity rating? so high-activity points with an even balance of friendly/unfriendly players are preferred
+ array<float> preferSpawnNodeRatings
+ foreach ( vector preferSpawnNode in file.preferSpawnNodes )
+ {
+ float currentRating
+
+ // this seems weird, not using rn
+ //Frontline currentFrontline = GetCurrentFrontline( team )
+ //if ( !IsFFAGame() || currentFrontline.friendlyCenter != < 0, 0, 0 > )
+ // currentRating += max( 0.0, ( 1000.0 - Distance2D( currentFrontline.origin, preferSpawnNode ) ) / 200 )
+
+ foreach ( entity nodePlayer in GetPlayerArray() )
+ {
+ float currentChange = 0.0
+
+ // the closer a player is to a node the more they matter
+ float dist = Distance2D( preferSpawnNode, nodePlayer.GetOrigin() )
+ if ( dist > 600.0 )
+ continue
+
+ currentChange = ( 600.0 - dist ) / 5
+ if ( player == nodePlayer )
+ currentChange *= -3 // always try to stay away from places we've already spawned
+ else if ( !IsAlive( nodePlayer ) ) // dead players mean activity which is good, but they're also dead so they don't matter as much as living ones
+ currentChange *= 0.6
+ if ( nodePlayer.GetTeam() != player.GetTeam() ) // if someone isn't on our team and alive they're probably bad
+ {
+ if ( IsFFAGame() ) // in ffa everyone is on different teams, so this isn't such a big deal
+ currentChange *= -0.2
+ else
+ currentChange *= -0.6
+ }
+
+ currentRating += currentChange
+ }
+
+ preferSpawnNodeRatings.append( currentRating )
+ }
+
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ float currentRating
+ float petTitanModifier
+ // scale how much a given spawnpoint matters to us based on how far it is from each node
+ bool spawnHasRecievedInitialBonus = false
+ for ( int i = 0; i < file.preferSpawnNodes.len(); i++ )
+ {
+ // bonus if autotitan is nearish
+ if ( IsAlive( player.GetPetTitan() ) && Distance( player.GetPetTitan().GetOrigin(), file.preferSpawnNodes[ i ] ) < 1200.0 )
+ petTitanModifier += 10.0
+
+ float dist = Distance2D( spawnpoint.GetOrigin(), file.preferSpawnNodes[ i ] )
+ if ( dist > 750.0 )
+ continue
+
+ if ( dist < 600.0 && !spawnHasRecievedInitialBonus )
+ {
+ currentRating += 10.0
+ spawnHasRecievedInitialBonus = true // should only get a bonus for simply being by a node once to avoid over-rating
+ }
+
+ currentRating += ( preferSpawnNodeRatings[ i ] * ( ( 750.0 - dist ) / 75 ) ) + max( RandomFloat( 1.25 ), 0.9 )
+ if ( dist < 250.0 ) // shouldn't get TOO close to an active node
+ currentRating *= 0.7
+
+ if ( spawnpoint.s.lastUsedTime < 10.0 )
+ currentRating *= 0.7
+ }
+
+ float rating = spawnpoint.CalculateRating( checkClass, team, currentRating, currentRating + petTitanModifier )
+ //print( "spawnpoint at " + spawnpoint.GetOrigin() + " has rating: " + )
+
+ if ( rating != 0.0 || currentRating != 0.0 )
+ print( "rating = " + rating + ", internal rating = " + currentRating )
+ }
+}
+
+void function OnSwitchingSides()
+{
+ file.sidesSwitched = true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/spawn_debug.gnut b/Northstar.CustomServers/scripts/vscripts/mp/spawn_debug.gnut
new file mode 100644
index 000000000..75ec8cf24
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/spawn_debug.gnut
@@ -0,0 +1,6 @@
+global function SpawnDebug_Init
+
+void function SpawnDebug_Init()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/spawn_on_friendly.gnut b/Northstar.CustomServers/scripts/vscripts/mp/spawn_on_friendly.gnut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/spawn_on_friendly.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/spawn_wave.gnut b/Northstar.CustomServers/scripts/vscripts/mp/spawn_wave.gnut
new file mode 100644
index 000000000..b8895c55c
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/spawn_wave.gnut
@@ -0,0 +1,6 @@
+global function SpawnWave_Init
+
+void function SpawnWave_Init()
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/mp/spawn_wave_dropship.gnut b/Northstar.CustomServers/scripts/vscripts/mp/spawn_wave_dropship.gnut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/mp/spawn_wave_dropship.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file