aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts/pilot
diff options
context:
space:
mode:
authorBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-08-31 23:14:58 +0100
committerBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-08-31 23:14:58 +0100
commit9a96d0bff56f1969c68bb52a2f33296095bdc67d (patch)
tree4175928e488632705692e3cccafa1a38dd854615 /Northstar.CustomServers/mod/scripts/vscripts/pilot
parent27bd240871b7c0f2f49fef137718b2e3c208e3b4 (diff)
downloadNorthstarMods-9a96d0bff56f1969c68bb52a2f33296095bdc67d.tar.gz
NorthstarMods-9a96d0bff56f1969c68bb52a2f33296095bdc67d.zip
move to new mod format
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/pilot')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/_leeching.gnut493
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/_pilot_leeching.gnut610
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/_slamzoom.nut85
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/_zipline.gnut838
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/pilot/class_wallrun.gnut224
5 files changed, 2250 insertions, 0 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/_leeching.gnut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_leeching.gnut
new file mode 100644
index 00000000..c9d1f9dd
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_leeching.gnut
@@ -0,0 +1,493 @@
+global function Leeching_Init
+
+global function DoLeechAnimEvent
+global function DoLeech
+global function TryLeechAbortCallback
+global function StartLeechingProgress
+global function StopLeechingProgress
+global function EnableLeeching
+global function DisableLeeching
+global function MarvinWeaponsFree
+global function GetLeechedEnts
+global function DataKnifeSuccessSounds
+global function DataKnifeCanceledSounds
+
+global function GetTeamLeechedEnts
+
+// _leeching.nut
+// sets up global stuff for leeching
+global const MARVIN_EMOTE_SOUND_HAPPY = "diag_spectre_gs_LeechEnd_01_1"
+global const MARVIN_EMOTE_SOUND_SAD = "diag_spectre_gs_LeechAborted_01_1"
+global const MARVIN_EMOTE_SOUND_PAIN = "diag_spectre_gs_LeechStart_01_1"
+
+#if MP
+global const bool WIFI_HACK_OVERFLOW_DIES = false // Kill a random leeched ent from within the team, exluding the current target, to create a new team member when hacked
+#elseif SP
+global const bool WIFI_HACK_OVERFLOW_DIES = true
+#endif
+
+struct LeechFuncInfo
+{
+ string classname
+ void functionref(entity,entity) DoLeech
+ void functionref(entity,entity) LeechStart
+ void functionref(entity,entity) LeechAbort
+}
+
+struct
+{
+ table<string, LeechFuncInfo> leechFuncs
+} file
+
+void function Leeching_Init()
+{
+ RegisterSignal( "OnLeeched" )
+ RegisterSignal( "OnStartLeech" )
+ RegisterSignal( "OnStopLeeched" )
+ RegisterSignal( "EnableLeeching" )
+
+ // Spectre leech
+ LeechFuncInfo spectre
+ spectre.classname = "npc_spectre"
+ spectre.DoLeech = LeechGeneric
+ spectre.LeechStart = LeechStartGeneric
+ spectre.LeechAbort = LeechAbortGeneric
+ file.leechFuncs[spectre.classname] <- spectre
+
+ // Reaper leech
+ LeechFuncInfo reaper
+ reaper.classname = "npc_super_spectre"
+ reaper.DoLeech = LeechGeneric
+ reaper.LeechStart = LeechStartGeneric
+ reaper.LeechAbort = LeechAbortGeneric
+ file.leechFuncs[reaper.classname] <- reaper
+
+ // Drone leech
+ LeechFuncInfo drone
+ drone.classname = "npc_drone"
+ drone.DoLeech = LeechGeneric
+ drone.LeechStart = LeechStartGeneric
+ drone.LeechAbort = LeechAbortGeneric
+ file.leechFuncs[drone.classname] <- drone
+
+ // Gunship leech
+ LeechFuncInfo gunship
+ gunship.classname = "npc_gunship"
+ gunship.DoLeech = LeechGeneric
+ gunship.LeechStart = LeechStartGeneric
+ gunship.LeechAbort = LeechAbortGeneric
+ file.leechFuncs[gunship.classname] <- gunship
+
+ LeechFuncInfo relay
+ relay.classname = "logic_relay"
+ relay.DoLeech = Leech_LogicRelay
+ file.leechFuncs[relay.classname] <- relay
+
+ LeechFuncInfo physbox
+ physbox.classname = "func_physbox"
+ physbox.DoLeech = Leech_FuncPhysbox
+ file.leechFuncs[physbox.classname] <- physbox
+}
+
+void function EnableLeeching( entity self )
+{
+ self.SetUsePrompts( "#DEFAULT_HACK_HOLD_PROMPT", "#DEFAULT_HACK_HOLD_PROMPT" )
+
+ Leech_SetLeechable( self )
+}
+
+void function DisableLeeching( entity self )
+{
+ if ( !IsValid_ThisFrame( self ) )
+ return
+
+ self.SetUsePrompts( " ", " " )
+
+ Leech_ClearLeechable( self )
+}
+
+void function StartLeechingProgress( entity self, entity leecher )
+{
+ self.Signal( "OnStartLeech" )
+ leecher.Signal( "OnStartLeech" )
+ self.ai.leechInProgress = true
+ self.ai.leechStartTime = Time()
+
+ TryLeechStartCallback( self, leecher )
+}
+
+void function StopLeechingProgress( entity self )
+{
+ self.ai.leechInProgress = false
+ self.ai.leechStartTime = -1
+}
+
+// called when any entity gets leeched
+void function DoLeechAnimEvent( entity self )
+{
+ entity leecher = expect entity( GetOptionalAnimEventVar( self, "leech_switchteam" ) )
+
+ DoLeech( self, leecher )
+}
+
+void function DoLeech( entity self, entity leecher )
+{
+ if ( !IsLeechable( self ) )
+ EnableLeeching( self )
+
+ Assert( "s" in self, "Self " + self + " has no .s" )
+ Assert( leecher )
+
+ // DEPRECATED- no scripts are currently using the results.player functionality- slayback
+ // logic_relays get Triggered when the object is leeched
+ //local results = {}
+ //results.player <- leecher
+ //self.Signal( "OnLeeched", results )
+ //leecher.Signal( "OnLeeched", results )
+
+ Signal( self, "OnLeeched" )
+ Signal( leecher, "OnLeeched" )
+
+ //DisableLeeching( self )
+
+ //_EnableLeechedPointMessage()
+
+ if ( leecher.IsPlayer() )
+ {
+ if ( self.IsNPC() )
+ self.SetBossPlayer( leecher )
+
+ TableRemoveDeadByKey( leecher.p.leechedEnts )
+
+ leecher.p.leechedEnts[ self ] <- self
+
+ // this will kill a random leeched ent from within the team, exluding the current target.
+ if ( WIFI_HACK_OVERFLOW_DIES )
+ ReleaseLeechOverflow( leecher, self )
+ }
+
+ if ( self.IsNPC() )
+ {
+ SetTeam( self, leecher.GetTeam() )
+ SetSquad( self, "" )
+ self.SetAutoSquad()
+ self.ClearPotentialThreatPos()
+ self.DisableBehavior( "Assault" )
+
+ foreach ( trigger in self.e.sonarTriggers )
+ {
+ OnSonarTriggerLeaveInternal( trigger, self )
+ }
+
+ #if DEV
+ // if crosshair spawned, switch squad so he isn't mixed in a squad with opposing team spectres
+ string squadname = expect string( self.kv.squadname )
+ if ( squadname.find( "crosshairSpawnSquad" ) != null )
+ self.SetSquad( "crosshairSpawnSquad_team_" + self.GetTeam() + "_" + self.GetClassName() )
+ #endif
+ }
+
+ // call a class specific leeching function for custom behavior
+ string targetCN = self.GetClassName()
+ if ( targetCN in file.leechFuncs )
+ {
+ LeechFuncInfo info = file.leechFuncs[ targetCN ]
+
+ // Assert( "DoLeech" in file.leechFuncs[ targetCN ] ) // not sure how to check legit functionref -slayback
+ void functionref(entity,entity) classLeechingFunc = file.leechFuncs[ targetCN ].DoLeech
+ thread classLeechingFunc( self, leecher )
+ }
+}
+
+array<entity> function GetTeamLeechedEnts( int team )
+{
+ array<entity> players = GetPlayerArrayOfTeam( team )
+ int totalCount = 0
+
+ array<entity> leechedArray
+ foreach ( player in players )
+ {
+ if ( IsValid( player ) && !player.IsBot() )
+ leechedArray.extend( GetLeechedEnts( player ) )
+ }
+
+ return leechedArray
+}
+
+array<entity> function GetLeechedEnts( entity leecher = null )
+{
+ array<entity> ents
+
+ foreach ( entity ent in leecher.p.leechedEnts )
+ {
+ if ( IsAlive( ent ) )
+ ents.append( ent )
+ }
+
+ return ents
+}
+
+void function TryLeechStartCallback( entity self, entity leecher )
+{
+ string leechtargetCN = self.GetClassName()
+ if ( leechtargetCN in file.leechFuncs )
+ {
+ if ( "LeechStart" in file.leechFuncs[ leechtargetCN ] )
+ {
+ void functionref(entity,entity) leechStartFunc = file.leechFuncs[ leechtargetCN ].LeechStart
+ thread leechStartFunc( self, leecher )
+ }
+ }
+}
+
+void function TryLeechAbortCallback( entity self, entity leecher )
+{
+ string leechtargetCN = self.GetClassName()
+ if ( leechtargetCN in file.leechFuncs )
+ {
+ if ( "LeechAbort" in file.leechFuncs[ leechtargetCN ] )
+ {
+ void functionref(entity,entity) leechAbortFunc = file.leechFuncs[ leechtargetCN ].LeechAbort
+ thread leechAbortFunc( self, leecher )
+ }
+ }
+}
+
+void function DataKnifeSuccessSounds( entity player )
+{
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "dataknife_complete", "dataknife_complete_3p", player, player )
+}
+
+void function DataKnifeCanceledSounds( entity player )
+{
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "dataknife_aborted", "dataknife_aborted_3p", player, player )
+}
+
+
+// --- CLASS SPECIFIC LEECH FUNCTIONS ---
+void function Leech_LogicRelay( entity self, entity leecher )
+{
+ Assert( self.GetClassName() == "logic_relay" )
+
+ // logic_relays get Triggered when the object is leeched
+ EntFire( self, "Trigger" )
+}
+
+void function Leech_FuncPhysbox( entity self, entity leecher )
+{
+ Assert( self.GetClassName() == "func_physbox" )
+
+ EntFire( self, "FireUser1" )
+}
+
+
+void function MarvinWeaponsFree( entity self )
+{
+ Assert( IsAlive( self ), self + " is dead, not alive!" )
+
+ // already have a weapon
+ if ( !self.GetActiveWeapon() )
+ return
+
+ self.EndSignal( "OnStopLeeched" )
+ self.EndSignal( "OnDeath" )
+ self.EndSignal( "OnTakeWeapon" )
+
+ OnThreadEnd(
+ function () : ( self )
+ {
+ if ( !IsAlive( self ) )
+ return
+ }
+ )
+
+ // its combat, time to get the hate on
+ EntFire( self, "UnholsterWeapon" )
+
+ WaitForever()
+}
+
+
+void function LeechStart_Marvin( entity self, entity leecher )
+{
+ //self.SetSkin( 4 )
+ EmitSoundOnEntity( self, MARVIN_EMOTE_SOUND_PAIN )
+}
+
+void function LeechAbort_Marvin( entity self, entity leecher )
+{
+ //self.SetSkin( 1 ) // happy
+ EmitSoundOnEntity( self, MARVIN_EMOTE_SOUND_SAD )
+}
+
+
+// Spectre leech
+
+void function Leech_Spectre( entity self, entity leecher )
+{
+ thread Leech_SpectreThread( self, leecher )
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+void function Leech_SpectreThread( entity self, entity leecher )
+{
+ Assert( self.GetClassName() == "npc_spectre" )
+
+ self.EndSignal( "OnDestroy" )
+ self.EndSignal( "OnDeath" )
+ self.EndSignal( "OnLeeched" )
+
+ EmitSoundOnEntity( self, MARVIN_EMOTE_SOUND_HAPPY )
+
+ Assert( leecher.IsPlayer() )
+
+ leecher.EndSignal( "OnDestroy" )
+
+ AddPlayerScore( leecher, "LeechSpectre" )
+
+ float timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( leecher, SFLAG_HUNTER_SPECTRE ) )
+ timerCredit *= 2.5
+ DecrementBuildTimer( leecher, timerCredit )
+
+ NPCFollowsPlayer( self, leecher )
+
+ OnThreadEnd(
+ function() : ( self, leecher )
+ {
+ // leecher is still connected so don't kill the spectre
+ if ( IsValid( leecher ) && !IsDisconnected( leecher ) )
+ return
+
+ // leecher is disconnected so kill the spectre
+ if ( IsAlive( self ) )
+ self.Die()
+ }
+ )
+
+ foreach ( callbackFunc in svGlobal.onLeechedCustomCallbackFunc )
+ {
+ callbackFunc( self, leecher )
+ }
+
+ WaitForever()
+}
+///////////////////////////////////////////////////////////////////////////////////////////////////////////
+void function LeechGeneric( entity self, entity leecher )
+{
+ thread LeechGenericThread( self, leecher )
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Run after the npc is successfully leeched
+// HACK: Should make this used by all leeched ents to avoid copy/pasted duplication. Will switch Spectre over to use this soon
+void function LeechGenericThread( entity self, entity leecher )
+{
+ Assert( IsValid( self ) )
+ self.EndSignal( "OnDestroy" )
+ self.EndSignal( "OnDeath" )
+ self.EndSignal( "OnLeeched" )
+
+ Assert( leecher.IsPlayer() )
+ leecher.EndSignal( "OnDestroy" )
+
+ string leechSound
+ float timerCredit
+
+ //--------------------------------------------
+ // Handle class-specific stuff
+ //---------------------------------------------
+ switch ( self.GetClassName() )
+ {
+ case "npc_spectre":
+ leechSound = MARVIN_EMOTE_SOUND_HAPPY
+ AddPlayerScore( leecher, "LeechSpectre" )
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ if ( PlayerHasServerFlag( leecher, SFLAG_HUNTER_SPECTRE ) )
+ timerCredit *= 2.5
+ break
+ case "npc_super_spectre":
+ leechSound = MARVIN_EMOTE_SOUND_HAPPY
+ AddPlayerScore( leecher, "LeechSuperSpectre" )
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ break
+ case "npc_drone":
+ leechSound = MARVIN_EMOTE_SOUND_HAPPY
+ AddPlayerScore( leecher, "LeechDrone" )
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ break
+ case "npc_gunship":
+ leechSound = MARVIN_EMOTE_SOUND_HAPPY
+ timerCredit = GetCurrentPlaylistVarFloat( "spectre_kill_credit", 0.5 )
+ break
+ default:
+ Assert( 0, "Unhandled hacked entity: " + self.GetClassName() )
+ }
+
+ EmitSoundOnEntity( self, leechSound )
+ DecrementBuildTimer( leecher, timerCredit )
+
+ // Multiplayer the leeched NPCs still follow the player, but in SP we don't want them to
+ if ( IsMultiplayer() )
+ NPCFollowsPlayer( self, leecher )
+
+ //--------------------------------------------
+ // Any leech custom callback funcs?
+ //---------------------------------------------
+ foreach ( callbackFunc in svGlobal.onLeechedCustomCallbackFunc )
+ {
+ callbackFunc( self, leecher )
+ }
+
+}
+
+/////////////////////////////////////////////////////////////////////////////////////
+void function LeechStartGeneric( entity self, entity leecher )
+{
+ string leechStartSound
+
+ switch( self.GetClassName() )
+ {
+ case "npc_spectre":
+ leechStartSound = MARVIN_EMOTE_SOUND_PAIN
+ break
+ case "npc_super_spectre":
+ leechStartSound = MARVIN_EMOTE_SOUND_PAIN
+ break
+ case "npc_drone":
+ leechStartSound = MARVIN_EMOTE_SOUND_PAIN
+ break
+ case "npc_gunship":
+ leechStartSound = MARVIN_EMOTE_SOUND_PAIN
+ break
+ }
+ Assert( leechStartSound != "", "Couldn't find leechStartSound for: " + self )
+
+ EmitSoundOnEntity( self, leechStartSound )
+}
+/////////////////////////////////////////////////////////////////////////////////////
+void function LeechAbortGeneric( entity self, entity leecher )
+{
+ string leechAbortSound
+
+ switch( self.GetClassName() )
+ {
+ case "npc_spectre":
+ leechAbortSound = MARVIN_EMOTE_SOUND_SAD
+ break
+ case "npc_super_spectre":
+ leechAbortSound = MARVIN_EMOTE_SOUND_SAD
+ break
+ case "npc_drone":
+ leechAbortSound = MARVIN_EMOTE_SOUND_SAD
+ break
+ case "npc_gunship":
+ leechAbortSound = MARVIN_EMOTE_SOUND_SAD
+ break
+ }
+ Assert( leechAbortSound != "", "Couldn't find leechAbortSound for: " + self )
+
+ EmitSoundOnEntity( self, leechAbortSound )
+
+}
+
+// --- END CLASS SPECIFIC LEECH FUNCTIONS --- \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/_pilot_leeching.gnut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_pilot_leeching.gnut
new file mode 100644
index 00000000..596ca711
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_pilot_leeching.gnut
@@ -0,0 +1,610 @@
+global function PlayerLeeching_Init
+
+global function LeechSurroundingSpectres
+global function CodeCallback_LeechStart
+global function LeechPropagate
+global function ReleaseLeechOverflow
+global function IsBeingLeeched
+
+// 384 ~ 32 feet
+// 256 ~ 21.3 feet
+// 192 ~ 16 feet
+// 128 ~ 10.6 feet
+const float SPECTRE_LEECH_SURROUNDING_RANGE = 384.0
+
+#if MP
+const int GLOBAL_LEECH_LIMIT = 100 // per team
+const int MAX_LEECHABLE = 100 // per player
+const bool PROPAGATE_ON_LEECH = true
+#elseif SP
+const int GLOBAL_LEECH_LIMIT = 8
+const int MAX_LEECHABLE = 4
+const bool PROPAGATE_ON_LEECH = false
+#endif
+
+void function PlayerLeeching_Init()
+{
+ #if SERVER
+ PrecacheModel( DATA_KNIFE_MODEL )
+ #endif
+}
+
+void function PlayerStopLeeching( entity player, entity target )
+{
+ Assert( target != null )
+ Assert( player.p.leechTarget == target )
+
+ StopLeechingProgress( player.p.leechTarget )
+
+ player.p.leechTarget = null
+}
+
+void function CodeCallback_LeechStart( entity player, entity target )
+{
+ thread LeechStartThread( player, target )
+}
+
+void function LeechStartThread( entity player, entity target )
+{
+ if ( !IsAlive( target ) )
+ return
+
+ if ( !IsAlive( player ) )
+ return
+
+ LeechActionInfo action = FindLeechAction( player, target )
+ if ( !action.isValid )
+ return
+
+/*
+ if ( player.ContextAction_IsActive()
+ || player.ContextAction_IsActive()
+ || target.ContextAction_IsActive() )
+ {
+ return
+ }
+*/
+
+ player.EndSignal( "ScriptAnimStop" )
+ player.EndSignal( "OnDeath" )
+ target.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDeath" )
+ target.EndSignal( "ScriptAnimStop" )
+
+ StartLeechingProgress( target, player )
+
+ LeechData e
+ e.playerStartOrg = player.GetOrigin()
+ e.targetStartPos = target.GetOrigin()
+
+ OnThreadEnd
+ (
+ function() : ( e, player, target )
+ {
+ if ( IsValid( player ) )
+ {
+ player.SetSyncedEntity( null )
+ if ( player.ContextAction_IsLeeching() )
+ player.Event_LeechEnd()
+
+ // reset to start position in case animation moves us at all
+ //player.SetOrigin( e.playerStartOrg )
+ player.Anim_Stop()
+ player.UnforceStand()
+
+ // done with first person anims
+ ClearPlayerAnimViewEntity( player )
+ DeployAndEnableWeapons( player )
+ }
+
+ if ( IsValid( target ) )
+ {
+ if ( !e.success )
+ {
+ if ( IsValid( player ) )
+ {
+ TryLeechAbortCallback( target, player ) //Make "failed leech" sounds play here after exiting leech animation
+ }
+ }
+
+ #if MP
+ target.SetUsable()
+ #endif
+ target.SetNoTarget( false )
+ target.SetNoTargetSmartAmmo( false )
+ target.Anim_Stop()
+ target.ClearParent()
+ if ( IsAlive( target ) )
+ {
+ // Note that e.targetStartPos is not guarranteed to be a safe spot since we can have moving geo in the game now
+ PutEntityInSafeSpot( target, null, null, target.GetOrigin(), target.GetOrigin() )
+ }
+
+ if ( target.ContextAction_IsLeeching() )
+ target.Event_LeechEnd()
+
+ }
+
+ foreach ( knife in e.knives )
+ {
+ if ( IsValid( knife ) )
+ {
+ knife.Destroy()
+ }
+
+ }
+
+ if ( IsValid( player ) && player.p.leechTarget )
+ {
+ PlayerStopLeeching( player, player.p.leechTarget )
+ }
+
+ if ( IsValid( e.ref ) )
+ {
+ if ( IsValid( player ) )
+ player.ClearParent()
+
+ if ( IsValid( target ) )
+ target.ClearParent()
+
+ //printt( "kill the ref" )
+ if ( IsValid( e.ref ) && !e.ref.IsPlayer() )
+ e.ref.Destroy()
+ }
+ }
+ )
+
+ Assert( player.p.leechTarget == null )
+ player.p.leechTarget = target
+ player.Event_LeechStart()
+ target.Event_LeechStart()
+ player.ForceStand()
+ HolsterAndDisableWeapons( player )
+
+ float leechTime = svGlobal.defaultPilotLeechTime
+ if ( PlayerHasPassive( player, ePassives.PAS_FAST_HACK ) )
+ leechTime *= 0.85
+
+ e.leechTime = leechTime
+
+ #if MP
+ target.UnsetUsable()
+ #endif
+ target.SetNoTarget( true )
+ target.SetNoTargetSmartAmmo( true )
+
+ if ( IsSpectre( target ) )
+ TellSquadmatesSpectreIsGettingLeeched( target, player )
+
+ waitthread PlayerLeechTargetAnimation( player, target, action, e )
+
+ e.leechStartTime = Time()
+ Remote_CallFunction_Replay( player, "ServerCallback_DataKnifeStartLeech", e.leechTime )
+ waitthread WaittillFinishedLeeching( player, target, e )
+
+ if ( e.success )
+ {
+ thread DataKnifeSuccessSounds( player )
+
+ DoLeech( target, player )
+ PlayerStopLeeching( player, target )
+
+ // this will kill a random leeched ent from within the team, exluding the current target. When it's not done elsewhere
+ if ( !WIFI_HACK_OVERFLOW_DIES )
+ ReleaseLeechOverflow( player, target )
+
+ //this is called when the player leeches - not when the system is leeching other spectres
+ if ( PROPAGATE_ON_LEECH && IsSpectre( target ) )
+ LeechSurroundingSpectres( target.GetOrigin(), player )
+ }
+ else
+ {
+ DataKnifeCanceledSounds( player )
+ Remote_CallFunction_Replay( player, "ServerCallback_DataKnifeCancelLeech" )
+ PlayerStopLeeching( player, player.p.leechTarget )
+ }
+
+ waitthread PlayerExitLeechingAnim( player, target, action, e )
+}
+
+void function TellSquadmatesSpectreIsGettingLeeched( entity spectre, entity player )
+{
+ string squadName = expect string( spectre.kv.squadname )
+ if ( squadName == "" )
+ return
+
+ array<entity> squad = GetNPCArrayBySquad( squadName )
+ squad.removebyvalue( spectre )
+
+ foreach ( squadMate in squad )
+ {
+ //printt( "Setting enemy of " + squadMate + " to player: " + player )
+ squadMate.SetEnemyLKP( player, player.GetOrigin() )
+ }
+}
+
+void function ReleaseLeechOverflow( entity player, entity lastLeeched )
+{
+ array<entity> teamLeechedEnts = GetTeamLeechedEnts( player.GetTeam() )
+ array<entity> leechedEnts = GetLeechedEnts( player )
+ int globalOverflow = GLOBAL_LEECH_LIMIT - teamLeechedEnts.len()
+ int playerOverflow = MAX_LEECHABLE - leechedEnts.len()
+
+ int overflow = minint( globalOverflow, playerOverflow )
+
+ if ( overflow >= 0 )
+ return
+
+ overflow = abs( overflow )
+
+ teamLeechedEnts.randomize()
+ foreach ( ent in teamLeechedEnts )
+ {
+ if ( lastLeeched == ent )
+ continue
+
+ entity owner = ent.GetBossPlayer()
+ Assert( owner.IsPlayer() )
+
+
+ // I think it's better to kill the overflow then have it become an enemy again.
+ ent.Die()
+
+ delete owner.p.leechedEnts[ ent ]
+ overflow--
+
+ if ( overflow == 0 )
+ break
+ }
+
+ Assert( overflow == 0 )
+}
+
+
+int function GetMaxNumberOfLeechedEnts( entity player )
+{
+ int teamLeechedCount = GetTeamLeechedEnts( player.GetTeam() ).len()
+ int leechedEntsCount = GetLeechedEnts( player ).len()
+ int teamLimit = maxint( 0, GLOBAL_LEECH_LIMIT - teamLeechedCount )
+ int maxSize = maxint( 0, MAX_LEECHABLE - leechedEntsCount )
+ maxSize = minint( teamLimit, maxSize )
+
+ return maxSize
+}
+
+void function LeechSurroundingSpectres( vector origin, entity player )
+{
+ array<entity> enemySpectreArray = GetNPCArrayEx( "npc_spectre", TEAM_ANY, player.GetTeam(), player.GetOrigin(), SPECTRE_LEECH_SURROUNDING_RANGE )
+
+ if ( !enemySpectreArray.len() )
+ return
+
+ // don't resize the array if we should kill the overflow instead
+ if ( !WIFI_HACK_OVERFLOW_DIES )
+ {
+ int maxSize = GetMaxNumberOfLeechedEnts( player )
+ int newSize = minint( enemySpectreArray.len(), maxSize )
+
+ enemySpectreArray.resize( newSize, null )
+ }
+
+ foreach ( spectre in enemySpectreArray )
+ {
+ thread LeechPropagate( spectre, player )
+ }
+
+ if ( enemySpectreArray.len() )
+ {
+ if ( PlayerHasPassive( player, ePassives.PAS_WIFI_SPECTRE ) )
+ {
+ EmitSoundOnEntity( player, "BurnCard_WiFiVirus_TurnSpectre" )
+ printt( "play BurnCard_WiFiVirus_TurnSpectre" )
+ }
+ }
+}
+
+void function LeechPropagate( entity spectre, entity player )
+{
+ if ( spectre.ContextAction_IsActive() )
+ return
+
+ if ( !spectre.IsInterruptable() )
+ return
+
+ if ( spectre.GetParent() )
+ return
+
+ if ( !Leech_IsLeechable( spectre ) )
+ return
+
+ player.EndSignal( "OnDestroy" )
+ spectre.EndSignal( "OnDestroy" )
+ spectre.EndSignal( "OnDeath" )
+
+ spectre.Event_LeechStart()
+
+ AddAnimEvent( spectre, "leech_switchteam", DoLeechAnimEvent, player )
+
+ OnThreadEnd(
+ function() : ( spectre )
+ {
+ if ( IsValid( spectre ) )
+ {
+ DeleteAnimEvent( spectre, "leech_switchteam" )
+
+ if ( spectre.ContextAction_IsLeeching() )
+ spectre.Event_LeechEnd()
+ }
+ }
+ )
+
+ spectre.Anim_Stop()
+ waitthread PlayAnim( spectre, "sp_reboot" )
+ spectre.SetVelocity( Vector(0,0,0) )
+}
+
+void function WaittillFinishedLeeching( entity player, entity target, LeechData e )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "ScriptAnimStop" )
+ target.EndSignal( "OnDeath" )
+
+ if ( !player.UseButtonPressed() )
+ return
+
+ float waitTime = e.leechTime
+ float timePassed = Time() - e.leechStartTime
+ waitTime -= timePassed
+ if ( waitTime > 0 )
+ {
+ float startTime = Time()
+ while ( Time() < startTime + waitTime && player.UseButtonPressed() )
+ {
+ WaitFrame()
+ }
+ }
+
+ if ( player.UseButtonPressed() )
+ e.success = true
+}
+
+/////////////////////////////////////////////////////////////
+bool function IsLeechTargetUsedAsAnimNode( entity target )
+{
+ return target.AISetting_LeechAnimTag() != ""
+}
+
+/////////////////////////////////////////////////////////////
+void function PlayerLeechTargetAnimation( entity player, entity target, LeechActionInfo action, LeechData e )
+{
+ Assert( action.isValid )
+ vector targetStartOrg = target.GetOrigin()
+ vector targetStartAng = target.GetAngles()
+
+ vector initialPlayerPosition = player.GetOrigin()
+ vector initialTargetPosition = target.GetOrigin()
+
+ vector endOrigin = target.GetOrigin()
+ vector startOrigin = player.GetOrigin()
+ vector refVec = endOrigin - startOrigin
+ string animTag
+
+ FirstPersonSequenceStruct playerSequence
+
+ //---------------------------------------------------------
+ // Leech anims played on the leech target, or at player position?
+ //---------------------------------------------------------
+ if ( IsLeechTargetUsedAsAnimNode( target ) )
+ {
+ e.ref = CreateLeechingScriptMoverBetweenEnts( player, target )
+ animTag = target.AISetting_LeechAnimTag()
+ Assert( animTag != "" )
+ e.ref.SetOrigin( target.GetOrigin() )
+ e.ref.SetParent( target, animTag )
+ }
+ else
+ {
+ e.ref = player
+ e.ref.SetOrigin( e.playerStartOrg )
+ playerSequence.playerPushable = true
+ }
+
+ e.ref.EndSignal( "OnDestroy" )
+
+ //-----------------------------------------------------------------
+ // Player FirstPersonSequence for the leeching
+ //-----------------------------------------------------------------
+ playerSequence.blendTime = 0.25
+ playerSequence.attachment = "ref"
+
+ //-----------------------------------------------------------------
+ // Only create FirstPersonSequence for leech target if anims exist
+ //-----------------------------------------------------------------
+ bool haveTargetSequence = false
+ FirstPersonSequenceStruct targetSequence
+
+ if ( action.targetAnimation3pStart != "" )
+ {
+ targetSequence = clone playerSequence
+ haveTargetSequence = true
+ }
+
+ playerSequence.thirdPersonAnim = action.playerAnimation3pStart
+ playerSequence.thirdPersonAnimIdle = action.playerAnimation3pIdle
+ playerSequence.firstPersonAnim = action.playerAnimation1pStart
+ playerSequence.firstPersonAnimIdle = action.playerAnimation1pIdle
+
+ entity viewmodel = player.GetFirstPersonProxy()
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt1" ) )
+ AddAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt1", PlaySound_DataKnife_Hack_Spectre_Pt1 )
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt2" ) )
+ AddAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt2", PlaySound_DataKnife_Hack_Spectre_Pt2 )
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt3" ) )
+ AddAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_Pt3", PlaySound_DataKnife_Hack_Spectre_Pt3 )
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_Spectre_Servo_Heavy_Short" ) )
+ AddAnimEvent( viewmodel, "PlaySound_Spectre_Servo_Heavy_Short", PlaySound_Spectre_Servo_Heavy_Short )
+
+ if ( !HasAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_ArmorRattle" ) )
+ AddAnimEvent( viewmodel, "PlaySound_DataKnife_Hack_Spectre_ArmorRattle", PlaySound_DataKnife_Hack_Spectre_ArmorRattle )
+
+ if ( haveTargetSequence )
+ {
+ targetSequence.thirdPersonAnim = action.targetAnimation3pStart
+ targetSequence.thirdPersonAnimIdle = action.targetAnimation3pIdle
+ }
+
+ playerSequence.noParent = true
+
+ //-----------------------------------
+ // Data knife
+ //-----------------------------------
+ asset model = DATA_KNIFE_MODEL
+
+ string knifeTag = GetTagForDataknife( target )
+ entity thirdPersonKnife = CreatePropDynamic( model )
+ SetTargetName( thirdPersonKnife, "thirdPersonKnife" )
+ thirdPersonKnife.SetParent( player, knifeTag, false, 0.0 )
+ e.knives.append( thirdPersonKnife )
+
+ SetForceDrawWhileParented( target, true )
+
+ //------------------------------------------------------------------------------
+ // Play leech anim sequence for player, but only for target if leech anims exist
+ //-------------------------------------------------------------------------------
+ player.SetSyncedEntity( target )
+ entity ref = e.ref
+ if ( haveTargetSequence )
+ thread Animate_PlayerLeechTarget( targetSequence, target, ref )
+
+ waitthread FirstPersonSequence( playerSequence, player, null )
+}
+
+
+//Basically copy pasted from CreateMeleeScriptMoverBetweenEnts
+entity function CreateLeechingScriptMoverBetweenEnts( entity attacker, entity target )
+{
+ vector endOrigin = target.GetOrigin()
+ vector startOrigin = attacker.GetOrigin()
+ vector refVec = endOrigin - startOrigin
+
+ vector refAng = VectorToAngles( refVec )
+ float pitch = refAng.x
+ if ( pitch > 180 )
+ pitch -= 360
+ if ( fabs( pitch ) > 35 ) //If pitch is too much, use angles from target
+ refAng = target.GetAngles() // Leech does it from behind target, so use target's angles.
+
+ vector refPos = endOrigin - refVec * 0.5
+
+ entity ref = CreateOwnedScriptMover( attacker )
+ ref.SetOrigin( refPos )
+ ref.SetAngles( refAng )
+
+ return ref
+}
+
+void function Animate_PlayerLeechTarget( FirstPersonSequenceStruct targetSequence, entity target, entity ref )
+{
+ ref.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDestroy" )
+ waitthread FirstPersonSequence( targetSequence, target, ref )
+}
+
+void function PlayerExitLeechingAnim( entity player, entity target, LeechActionInfo action, LeechData e )
+{
+ FirstPersonSequenceStruct playerSequence
+ playerSequence.blendTime = 0.3
+ playerSequence.attachment = "ref"
+ playerSequence.teleport = false
+ playerSequence.noParent = true
+ playerSequence.playerPushable = true
+
+ //--------------------------------------
+ // Target animates only if he has anims
+ //---------------------------------------
+ bool hasTargetSequence = false
+ FirstPersonSequenceStruct targetSequence
+ if ( action.targetAnimation3pEnd != "" )
+ {
+ targetSequence = clone playerSequence
+ hasTargetSequence = true
+ }
+
+ playerSequence.thirdPersonAnim = action.playerAnimation3pEnd
+ playerSequence.firstPersonAnim = action.playerAnimation1pEnd
+ playerSequence.snapPlayerFeetToEyes = false
+
+ entity ref = e.ref
+
+ if ( hasTargetSequence )
+ {
+ targetSequence.thirdPersonAnim = action.targetAnimation3pEnd
+ thread FirstPersonSequence( targetSequence, target, ref )
+ }
+ waitthread FirstPersonSequence( playerSequence, player, null )
+
+ //-------------------------------------------------------------
+ // Detach from rodeo if applicable (drones, superspectres, etc)
+ //-------------------------------------------------------------
+ if ( Rodeo_IsAttached( player ) )
+ player.Signal( "RodeoOver" )
+}
+
+bool function IsBeingLeeched( entity npc )
+{
+ return npc.ai.leechInProgress
+}
+
+void function PlaySound_DataKnife_Hack_Spectre_Pt1( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "DataKnife_Hack_Spectre_Pt1" )
+
+}
+
+void function PlaySound_DataKnife_Hack_Spectre_Pt2( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "DataKnife_Hack_Spectre_Pt2" )
+
+}
+
+void function PlaySound_DataKnife_Hack_Spectre_Pt3( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "DataKnife_Hack_Spectre_Pt3" )
+
+}
+
+void function PlaySound_Spectre_Servo_Heavy_Short( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Spectre.Servo.Heavy.Short" )
+
+}
+
+void function PlaySound_DataKnife_Hack_Spectre_ArmorRattle( entity playerFirstPersonProxy )
+{
+ entity player = playerFirstPersonProxy.GetOwner()
+ if ( !IsValid( player ) )
+ return
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "DataKnife_Hack_Spectre_ArmorRattle" )
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/_slamzoom.nut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_slamzoom.nut
new file mode 100644
index 00000000..83ee3916
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_slamzoom.nut
@@ -0,0 +1,85 @@
+untyped
+
+global function ShouldSlamzoomSpawn
+global function SlammzoomSpawn
+
+const SLAMZOOM_WHOOSH_SOUND = "UI_InGame_MarkedForDeath_PlayerUnmarked"
+const SLAMZOOM_COLOR_CORRECTION = "materials/correction/player_electric_damage.raw"
+
+function ShouldSlamzoomSpawn()
+{
+ return ( GetCurrentPlaylistVarInt( "enable_slamzoom_spawn", 0 ) == 1 )
+}
+
+function SlammzoomSpawn( entity player, vector origin, vector angles, entity friendlyPilot = null )
+{
+ player.EndSignal( "OnDestroy" )
+ player.SetOrigin( origin )
+ player.SnapEyeAngles( angles )
+
+ vector landorigin = player.EyePosition()
+
+ // slamzoom
+ int slamzoom_height = 4000
+ float slamzoom_time1 = 0.5
+ float slamzoom_time2 = 0.7
+ float slamzoom_rotate_time = 0.3
+ int slamzoom_angle = 90
+ int enter_angle = 90 - slamzoom_angle
+
+ vector start_angles = Vector( -90-enter_angle, angles.y, 0 )
+ vector start_vector = AnglesToForward( start_angles )
+
+ // origin = origin + Vector(0,0,48)
+
+ entity camera = CreateTitanDropCamera( origin + start_vector * slamzoom_height, Vector( slamzoom_angle, angles.y, 0.0) )
+ camera.Fire( "Enable", "!activator", 0, player )
+
+ entity mover = CreateScriptMover()
+ if ( IsValid( camera ) )
+ {
+ // camera can be invalid for a moment when server shuts down
+ mover.SetOrigin( camera.GetOrigin() )
+ mover.SetAngles( camera.GetAngles() )
+ camera.SetParent( mover )
+ }
+
+ OnThreadEnd(
+ function() : ( mover, camera )
+ {
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ if ( IsValid( camera ) )
+ camera.Destroy()
+ }
+ )
+
+ player.isSpawning = mover
+ mover.MoveTo( mover.GetOrigin() + (start_vector * 100), slamzoom_time1, slamzoom_time1*0.4, slamzoom_time1*0.4 )
+ wait slamzoom_time1
+ EmitSoundOnEntityOnlyToPlayer( player, player, SLAMZOOM_WHOOSH_SOUND )
+ mover.MoveTo( landorigin, slamzoom_time2, slamzoom_time2*0.5, slamzoom_time2*0.2 )
+ wait slamzoom_time2 - slamzoom_rotate_time
+ mover.RotateTo( angles, slamzoom_rotate_time, slamzoom_rotate_time*0.2, slamzoom_rotate_time*0.2 )
+ wait slamzoom_rotate_time
+ player.isSpawning = null
+ wait 0.1
+ if ( IsValid( camera ) )
+ {
+ // camera can be invalid for a moment when server shuts down
+ camera.FireNow( "Disable", "!activator", null, player )
+ }
+
+ if ( IsValid( friendlyPilot ) )
+ {
+ MessageToPlayer( friendlyPilot, eEventNotifications.FriendlyPlayerSpawnedOnYou, player )
+ MessageToPlayer( player, eEventNotifications.SpawnedOnFriendlyPlayer, friendlyPilot )
+ }
+
+ if ( ShouldGivePlayerInfoOnSpawn() )
+ thread GivePlayerInfoOnSpawn( player )
+
+ player.SetOrigin( origin )
+ player.SnapEyeAngles( angles )
+ player.RespawnPlayer( null )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/_zipline.gnut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_zipline.gnut
new file mode 100644
index 00000000..a129c479
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/_zipline.gnut
@@ -0,0 +1,838 @@
+untyped
+
+global function Zipline_Init
+
+global function GuyZiplinesToGround
+global function GetZiplineSpawns
+global function GetHookOriginFromNode
+global function ZiplineInit
+
+global function CodeCallback_ZiplineMount
+global function CodeCallback_ZiplineStart
+global function CodeCallback_ZiplineMove
+global function CodeCallback_ZiplineStop
+
+global function AddCallback_ZiplineStart
+global function AddCallback_ZiplineStop
+
+global function TrackMoverDirection
+global function CreateRopeEntities
+global function SpawnZiplineEntities
+global function GetZiplineLandingAnims
+global function AnimDoneStuckInSolidFailSafe
+
+struct {
+ array<string> zipLineLandingAnimations = [
+ "pt_zipline_dismount_standF",
+ "pt_zipline_dismount_crouchF",
+ "pt_zipline_dismount_crouch180",
+ "pt_zipline_dismount_breakright",
+ "pt_zipline_land"
+ "pt_zipline_land"
+ "pt_zipline_land"
+ "pt_zipline_land"
+ "pt_zipline_land"
+ "pt_zipline_land"
+ ]
+
+ array<string> zipLinePlayerLandingAnimations = [
+ "pt_zipline_dismount_standF"
+ ]
+
+ array<string> zipLineReadyAnimations = [
+ "pt_zipline_ready_idleA",
+ "pt_zipline_ready_idleB"
+ ]
+} file
+
+//typedef EntitiesDidLoadCallbackType void functionref(entity)
+array<void functionref(entity,entity)> _ZiplineStartCallbacks
+array<void functionref(entity)> _ZiplineStopCallbacks
+
+function Zipline_Init()
+{
+ if ( reloadingScripts )
+ return
+
+ RegisterSignal( "deploy" )
+ RegisterSignal( "stop_deploy" )
+ RegisterSignal( "npc_deployed" )
+
+ VehicleDropshipNew_Init()
+
+ level.MIN_ZIPLINE_LAND_DIST_SQRD <- 128 * 128
+ level.MIN_ZIPLINE_HOOK_DIST_SQRD <- 256 * 256
+ level._info_spawnpoint_dropships <- {}
+ AddSpawnCallback( "info_spawnpoint_dropship", AddToSpawnpointDropships )
+
+ PrecacheParticleSystem( $"hmn_mcorps_jump_jet_wallrun_full" )
+ PrecacheParticleSystem( $"hmn_imc_jump_jet_wallrun_full" )
+ PrecacheParticleSystem( $"P_Zipline_hld_1" )
+
+}
+
+void function AddToSpawnpointDropships( entity self )
+{
+ level._info_spawnpoint_dropships[ self ] <- self
+}
+
+function GetZiplineSpawns()
+{
+ local targets = []
+ foreach ( ent in clone level._info_spawnpoint_dropships )
+ {
+ if ( IsValid( ent ) )
+ {
+ targets.append( ent )
+ continue
+ }
+
+ delete level._info_spawnpoint_dropships[ ent ]
+ }
+
+ return targets
+}
+
+
+function GuyZiplinesToGround( guy, Table )
+{
+ expect entity( guy )
+
+ OnThreadEnd(
+ function() : ( guy )
+ {
+ if ( IsValid( guy ) )
+ guy.SetEfficientMode( false )
+ }
+ )
+
+ local ship = Table.ship
+ local dropPos = GetDropPos( Table )
+
+ // ship didn't find a drop spot
+ if ( dropPos == null )
+ WaitForever()
+
+ //DebugDrawLine( guy.GetOrigin(), dropPos, 255, 0, 0, true, 8.0 )
+
+ local attachOrigin = ship.GetAttachmentOrigin( Table.attachIndex )
+ local nodeOrigin = dropPos
+ local hookOrigin = GetHookOriginFromNode( guy.GetOrigin(), nodeOrigin, attachOrigin )
+
+ // couldn't find a place to hook it? This needs to be tested on precompile
+ if ( !hookOrigin )
+ {
+ printt( "WARNING! Bad zipline dropship position!" )
+ WaitForever()
+ }
+
+ Table.hookOrigin <- hookOrigin
+
+ // Track the movement of the script mover that moves the guy to the ground
+ local e = {}
+
+ waitthread GuyRidesZiplineToGround( guy, Table, e, dropPos )
+
+ //DebugDrawLine( guy.GetOrigin(), dropPos, 255, 0, 135, true, 5.0 )
+
+ if ( !( "forward" in Table ) )
+ {
+ // the sequence ended before the guy reached the ground
+ local start = guy.GetOrigin()
+ // this needs functionification
+ local end = Table.hookOrigin + Vector( 0,0,-80 )
+ TraceResults result = TraceLine( start, end, guy )
+ local angles = guy.GetAngles()
+ Table.forward <- AnglesToForward( angles )
+ Table.origin <- result.endPos
+ }
+
+ // the guy detaches and falls to the ground
+ string landingAnim = file.zipLineLandingAnimations.getrandom()
+ //DrawArrow( guy.GetOrigin(), guy.GetAngles(), 5.0, 80 )
+
+ if ( !guy.IsInterruptable() )
+ return
+
+ guy.Anim_ScriptedPlay( landingAnim )
+ guy.Anim_EnablePlanting()
+
+ ShowName( guy )
+
+ local vec = e.currentOrigin - e.oldOrigin
+
+ guy.SetVelocity( vec * 15 )
+
+ thread AnimDoneStuckInSolidFailSafe( guy )
+}
+
+function AnimDoneStuckInSolidFailSafe( entity guy )
+{
+ guy.EndSignal( "OnDeath" )
+ guy.WaitSignal( "OnAnimationDone" )
+
+ if ( EntityInSolid( guy ) )
+ {
+ vector ornull clampedPos
+ clampedPos = NavMesh_ClampPointForAIWithExtents( guy.GetOrigin(), guy, < 400, 400, 400 > )
+
+ if ( clampedPos != null )
+ {
+ guy.SetOrigin( expect vector( clampedPos ) )
+ printt( guy + " was in solid, teleported" )
+ }
+ }
+}
+
+function TrackMoverDirection( mover, e )
+{
+ mover.EndSignal( "OnDestroy" )
+ // track the way the mover movers, so we can do the
+ // correct velocity on the falling guy
+ local origin = mover.GetOrigin()
+ e.oldOrigin <- origin
+ e.currentOrigin <- origin
+
+ for ( ;; )
+ {
+ WaitFrame()
+ e.oldOrigin = e.currentOrigin
+ e.currentOrigin = mover.GetOrigin()
+ }
+}
+
+function GuyRidesZiplineToGround( entity guy, zipline, e, dropPos )
+{
+ entity mover = CreateOwnedScriptMover( guy )
+ mover.EndSignal( "OnDestroy" )
+
+ thread TrackMoverDirection( mover, e )
+
+ OnThreadEnd(
+ function() : ( mover, zipline, guy )
+ {
+ thread ZiplineRetracts( zipline )
+
+ if ( IsValid( guy ) )
+ {
+ guy.ClearParent()
+ StopSoundOnEntity( guy, "3p_zipline_loop" )
+ EmitSoundOnEntity( guy, "3p_zipline_detach" )
+ }
+
+ if ( IsValid( mover ) )
+ mover.Kill_Deprecated_UseDestroyInstead()
+ }
+ )
+
+
+ local rideDist = Distance( guy.GetOrigin(), zipline.hookOrigin )
+
+ // how long it takes the zipline to travel 1000 units
+ zipline.pinTime <- Graph( rideDist, 0, 1000, 0, 0.4 )
+
+ // how long it takes the zipline to retract,
+ zipline.retractTime <- Graph( rideDist, 0, 1000, 0, 0.5 )
+
+ // how long it takes the rider to ride 1000 units
+ float rideTime = Graph( rideDist, 0, 1000, 0, 2.5 )
+
+
+ // orient the script_mover in the direction its going
+ local angles = guy.GetAngles()
+ local forward = AnglesToForward( angles )
+ local right = AnglesToRight( angles )
+
+ CreateRopeEntities( zipline )
+
+ local zipAttachOrigin = zipline.ship.GetAttachmentOrigin( zipline.attachIndex )
+ zipline.end.SetOrigin( zipAttachOrigin )
+
+ zipline.start.SetParent( zipline.ship, zipline.shipAttach )
+ zipline.mid.SetParent( zipline.ship, zipline.shipAttach )
+
+ // now that the origin is set we can spawn the zipline, otherwise we
+ // see the zipline lerp in
+ SpawnZiplineEntities( zipline )
+
+
+ // the zipline shoots out
+ ZiplineMover( expect entity( zipline.end ), zipline.hookOrigin, zipline.pinTime )
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, zipAttachOrigin, "dropship_zipline_zipfire" )
+ delaythread( zipline.pinTime ) ZiplineMoveCleanup( zipline )
+
+// wait zipline.pinTime * 0.37
+ wait zipline.pinTime
+ EmitSoundAtPosition( TEAM_UNASSIGNED, zipline.hookOrigin, "dropship_zipline_impact" )
+
+ zipline.mid.SetParent( mover, "ref", false )
+ thread MoverMovesToGround( zipline, mover, rideTime )
+
+ if ( !IsAlive( guy ) || !guy.IsInterruptable() )
+ return
+
+ guy.SetParent( mover, "ref", false, 0.0 )
+
+ EmitSoundOnEntity( guy, "3p_zipline_attach" )
+ waitthread PlayAnim( guy, "pt_zipline_ready2slide", mover )
+ EmitSoundOnEntity( guy, "3p_zipline_loop" )
+
+ if ( !IsAlive( guy ) || !guy.IsInterruptable() || guy.GetParent() != mover )
+ return
+
+ // Anim_PlayWithRefPoint requires that the guy be parented to the ref point.
+ thread PlayAnim( guy, ZIPLINE_IDLE_ANIM, mover, "ref" )
+
+ //thread ZiplineAutoClipsToGeo( zipline, mover )
+
+ //wait 0.4 // some time to clear the lip
+
+ local nodeOrigin = dropPos
+ //DebugDrawLine( guy.GetOrigin(), nodeOrigin, 200, 255, 50, true, 8.0 )
+
+ rideDist = Distance( guy.GetOrigin(), nodeOrigin )
+ rideDist -= 100 // for animation at end
+ if ( rideDist < 0 )
+ rideDist = 0
+ rideTime = Graph( rideDist, 0, 100, 0, 0.15 )
+/*
+ printt( "ride time " + rideTime )
+ local endTime = Time() + rideTime
+ for ( ;; )
+ {
+ if ( Time() >= endTime )
+ return
+
+ DebugDrawLine( guy.GetOrigin(), nodeOrigin, 255, 0, 0, true, 0.15 )
+ DebugDrawText( nodeOrigin, ( endTime - Time() ) + "", true, 0.15 )
+ WaitFrame()
+ }
+*/
+ wait rideTime
+
+ thread ZiplineStuckFailsafe( guy, nodeOrigin )
+}
+
+function ZiplineStuckFailsafe( guy, nodeOrigin )
+{
+ TimeOut( 15.0 )
+
+ guy.EndSignal( "OnDeath" )
+
+ guy.WaitSignal( "OnFailedToPath" )
+
+ guy.SetOrigin( nodeOrigin )
+ printt( "Warning: AI Path failsafe at " + nodeOrigin )
+}
+
+function ZiplineMoveCleanup( zipline )
+{
+ // work around for moveto bug
+ if ( IsValid( zipline.end ) )
+ {
+ zipline.end.SetOrigin( zipline.hookOrigin )
+ }
+}
+
+function MoverMovesToGround( zipline, mover, timeTotal )
+{
+ // this handles the start point moving.
+ mover.EndSignal( "OnDestroy" )
+ zipline.ship.EndSignal( "OnDestroy" )
+
+ local origin = zipline.ship.GetAttachmentOrigin( zipline.attachIndex )
+ local angles = zipline.ship.GetAttachmentAngles( zipline.attachIndex )
+ mover.SetOrigin( origin )
+ mover.SetAngles( angles )
+
+ local start = zipline.start.GetOrigin()
+ local end = zipline.hookOrigin + Vector( 0,0,-180 )
+
+ local blendTime = 0.5
+ if ( timeTotal <= blendTime )
+ blendTime = 0
+
+ angles = VectorToAngles( end - start )
+ angles.x = 0
+ angles.z = 0
+
+ mover.MoveTo( end, timeTotal, blendTime, 0 )
+ mover.RotateTo( angles, 0.2 )
+}
+
+
+function WaitUntilZiplinerNearsGround( guy, zipline )
+{
+ local start, end, frac
+ local angles = guy.GetAngles()
+ local forward = AnglesToForward( angles )
+
+ local zipAngles, zipForward, dropDist
+
+ if ( guy.IsNPC() )
+ dropDist = 150
+ else
+ dropDist = 10 //much closer for player
+
+ local mins = guy.GetBoundingMins()
+ local maxs = guy.GetBoundingMaxs()
+
+ TraceResults result
+
+ for ( ;; )
+ {
+ start = guy.GetOrigin()
+ end = start + Vector(0,0,-dropDist)
+ end += forward * dropDist
+// TraceResults result = TraceLine( start, end, guy )
+ result = TraceHull( start, end, mins, maxs, guy, TRACE_MASK_NPCSOLID_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( start, end, 255, 0, 0, true, 0.2 )
+
+ if ( result.fraction < 1.0 )
+ break
+
+ start = guy.GetOrigin()
+ end = zipline.hookOrigin + Vector( 0,0,-80 )
+
+ zipForward = ( end - start )
+ zipForward.Norm()
+ zipForward *= 250
+
+ end = start + zipForward
+ //DebugDrawLine( start, end, 255, 0, 0, true, 0.1 )
+
+// result = TraceLine( start, end, guy )
+ //DebugDrawLine( start, end, 255, 150, 0, true, 0.2 )
+ result = TraceHull( start, end, mins, maxs, guy, TRACE_MASK_NPCSOLID_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+
+ if ( result.fraction < 1.0 )
+ break
+
+ WaitFrame()
+ }
+
+ zipline.origin <- result.endPos
+ zipline.forward <- forward
+}
+
+
+function ZiplineRetracts( zipline )
+{
+ if ( !IsValid( zipline.start ) )
+ return
+ if ( !IsValid( zipline.mid ) )
+ return
+ if ( !IsValid( zipline.end ) )
+ return
+
+ OnThreadEnd(
+ function() : ( zipline )
+ {
+ if ( IsValid( zipline.start ) )
+ zipline.start.Kill_Deprecated_UseDestroyInstead()
+
+ if ( IsValid( zipline.mid ) )
+ zipline.mid.Kill_Deprecated_UseDestroyInstead()
+
+ // is the only one that's not parented and only gets deleted here
+ zipline.end.Kill_Deprecated_UseDestroyInstead()
+ }
+ )
+
+ // IsValid check succeeds, even if a delete brought us here.
+ // IsValid should've failed.
+ if ( !IsAlive( expect entity( zipline.ship ) ) )
+ return
+
+ zipline.ship.EndSignal( "OnDestroy" )
+
+ zipline.start.EndSignal( "OnDestroy" )
+ zipline.mid.EndSignal( "OnDestroy" )
+ zipline.end.EndSignal( "OnDestroy" )
+
+ local start, end, mid
+ local startDist
+ local endDist
+ local totalDist
+ local progress
+ local newMidPoint
+ local midRetractProgress
+
+ local startTime = Time()
+ local endTime = startTime + 0.3
+
+ zipline.mid.ClearParent()
+
+ start = zipline.start.GetOrigin()
+ end = zipline.end.GetOrigin()
+ mid = zipline.mid.GetOrigin()
+
+ startDist = Distance( mid, start )
+ endDist = Distance( mid, end )
+ totalDist = startDist + endDist
+
+ if ( totalDist <= 0 )
+ return
+
+ progress = startDist / totalDist
+// newMidPoint = end * progress + start * ( 1 - progress )
+//
+// // how far from the midpoint we are, vertically
+// local mid_z_offset = newMidPoint.z - mid.z
+// local addOffset
+
+ for ( ;; )
+ {
+ start = zipline.start.GetOrigin()
+ end = zipline.end.GetOrigin()
+
+ newMidPoint = end * progress + start * ( 1 - progress )
+
+ midRetractProgress = GraphCapped( Time(), startTime, endTime, 0, 1 )
+ if ( midRetractProgress >= 1.0 )
+ break
+
+ newMidPoint = mid * ( 1 - midRetractProgress ) + newMidPoint * midRetractProgress
+ //addOffset = mid_z_offset * ( 1 - midRetractProgress )
+ //newMidPoint.z -= addOffset
+ //DebugDrawLine( zipline.mid.GetOrigin(), newMidPoint, 255, 0, 0, true, 0.2 )
+
+ if ( !IsValid( zipline.mid ) )
+ {
+ printt( "Invalid zipline mid! Impossible!" )
+ }
+ else
+ {
+ zipline.mid.SetOrigin( newMidPoint )
+ }
+
+
+// startDist = Distance( mid, start )
+// endDist = Distance( mid, end )
+// totalDist = startDist + endDist
+// progress = startDist / totalDist
+ WaitFrame()
+ }
+
+// DebugDrawLine( zipline.start.GetOrigin(), zipline.mid.GetOrigin(), 255, 100, 50, true, 5.0 )
+// DebugDrawLine( zipline.end.GetOrigin(), zipline.mid.GetOrigin(), 60, 100, 244, true, 5.0 )
+ local moveTime = 0.4
+ ZiplineMover( expect entity( zipline.start ), zipline.end.GetOrigin(), moveTime )
+ ZiplineMover( expect entity( zipline.mid ), zipline.end.GetOrigin(), moveTime )
+
+ wait moveTime
+/*
+ startTime = Time()
+ endTime = startTime + zipline.retractTime
+ end = zipline.end.GetOrigin()
+
+ if ( !IsValid( zipline.mid ) )
+ return
+ mid = zipline.mid.GetOrigin()
+
+ local org
+
+ for ( ;; )
+ {
+ start = zipline.start.GetOrigin()
+
+ progress = Graph( Time(), startTime, endTime )
+ if ( progress >= 1.0 )
+ break
+
+ org = end * ( 1 - progress ) + start * progress
+ zipline.end.SetOrigin( org )
+
+ org = mid * ( 1 - progress ) + start * progress
+
+ if ( !IsValid( zipline.mid ) )
+ return
+ zipline.mid.SetOrigin( org )
+
+ WaitFrame()
+ }
+*/
+}
+
+function CreateRopeEntities( Table )
+{
+ local subdivisions = 8 // 25
+ local slack = 100 // 25
+ string midpointName = UniqueString( "rope_midpoint" )
+ string endpointName = UniqueString( "rope_endpoint" )
+
+ entity rope_start = CreateEntity( "move_rope" )
+ rope_start.kv.NextKey = midpointName
+ rope_start.kv.MoveSpeed = 64
+ rope_start.kv.Slack = slack
+ rope_start.kv.Subdiv = subdivisions
+ rope_start.kv.Width = "2"
+ rope_start.kv.TextureScale = "1"
+ rope_start.kv.RopeMaterial = "cable/cable.vmt"
+ rope_start.kv.PositionInterpolator = 2
+
+ entity rope_mid = CreateEntity( "keyframe_rope" )
+ SetTargetName( rope_mid, midpointName )
+ rope_mid.kv.NextKey = endpointName
+ rope_mid.kv.MoveSpeed = 64
+ rope_mid.kv.Slack = slack
+ rope_mid.kv.Subdiv = subdivisions
+ rope_mid.kv.Width = "2"
+ rope_mid.kv.TextureScale = "1"
+ rope_mid.kv.RopeMaterial = "cable/cable.vmt"
+
+ entity rope_end = CreateEntity( "keyframe_rope" )
+ SetTargetName( rope_end, endpointName )
+ rope_end.kv.MoveSpeed = 64
+ rope_end.kv.Slack = slack
+ rope_end.kv.Subdiv = subdivisions
+ rope_end.kv.Width = "2"
+ rope_end.kv.TextureScale = "1"
+ rope_end.kv.RopeMaterial = "cable/cable.vmt"
+
+ Table.start <- rope_start
+ Table.mid <- rope_mid
+ Table.end <- rope_end
+
+ return Table
+}
+
+function SpawnZiplineEntities( Table )
+{
+ // after origins are set
+ DispatchSpawn( Table.start )
+ DispatchSpawn( Table.mid )
+ DispatchSpawn( Table.end )
+ return Table
+}
+
+function GetDropPos( zipline )
+{
+ entity ship = expect entity( zipline.ship )
+ if ( !HasDropshipDropTable( ship ) )
+ return null
+
+ DropTable dropTable = GetDropshipDropTable( ship )
+
+ foreach ( side, nodeTables in dropTable.nodes )
+ {
+ foreach ( nodeTable in nodeTables )
+ {
+ if ( nodeTable.attachName == zipline.shipAttach )
+ return nodeTable.origin
+ }
+ }
+
+ return null
+}
+
+function GetHookOriginFromNode( origin, nodeOrigin, attachOrigin )
+{
+ // need to use the slope of guy to node to get the slope for the zipline, then launch it from the attachment origin
+ local dropVec = nodeOrigin - origin
+ local dropDist = Length( dropVec )
+ dropVec.Norm()
+
+// DrawArrow( nodeOrigin, Vector(0,0,0), 5, 100 )
+ local attachEnd = attachOrigin + dropVec * ( dropDist + 1500 ) // some buffer
+ TraceResults zipTrace = TraceLine( attachOrigin, attachEnd, null, TRACE_MASK_NPCWORLDSTATIC )
+
+// DebugDrawLine( attachOrigin, zipTrace.endPos, 0, 255, 0, true, 5.0 )
+// DebugDrawLine( zipTrace.endPos, attachEnd, 255, 0, 0, true, 5.0 )
+
+ // zipline didn't connect with anything
+ if ( zipTrace.fraction == 1.0 )
+ {
+// DebugDrawLine( attachOrigin, attachEnd, 255, 255, 0, true, 5.0 )
+ return null
+ }
+
+ if ( Distance( zipTrace.endPos, attachOrigin ) < 300 )
+ return null
+
+ return zipTrace.endPos
+}
+
+function ZiplineInit( entity player )
+{
+ player.s.ziplineEffects <- []
+}
+
+function CreateZiplineJetEffects( entity player )
+{
+ asset jumpJetEffectFriendlyName = $"hmn_imc_jump_jet_wallrun_full"
+ asset jumpJetEffectEnemyName = $"hmn_mcorps_jump_jet_wallrun_full"
+ int playerTeam = player.GetTeam()
+
+ //HACK!
+ //Create 2 sets of jump jet effects, 1 visible to friendly, 1 visible to enemy
+ //Doing this for a myriad of reasons on the server as opposed to on the client like the rest
+ //of the jump jet effects. Since ziplining isn't all that common an action it should be fine
+
+ //create left jump jetfriendly
+ entity leftJumpJetFriendly = CreateEntity( "info_particle_system" )
+ leftJumpJetFriendly.kv.start_active = 1
+ leftJumpJetFriendly.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY
+ leftJumpJetFriendly.SetValueForEffectNameKey( jumpJetEffectFriendlyName )
+ SetTargetName( leftJumpJetFriendly, UniqueString() )
+ leftJumpJetFriendly.SetParent( player, "vent_left_back", false, 0 )
+ SetTeam( leftJumpJetFriendly, playerTeam )
+ leftJumpJetFriendly.SetOwner( player)
+ DispatchSpawn( leftJumpJetFriendly )
+
+ //now create right jump jet for friendly
+ entity rightJumpJetFriendly = CreateEntity( "info_particle_system" )
+ rightJumpJetFriendly.kv.start_active = 1
+ rightJumpJetFriendly.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY
+ rightJumpJetFriendly.SetValueForEffectNameKey( jumpJetEffectFriendlyName )
+ SetTargetName( rightJumpJetFriendly, UniqueString() )
+ rightJumpJetFriendly.SetParent( player, "vent_right_back", false, 0 )
+ SetTeam( rightJumpJetFriendly, playerTeam )
+ rightJumpJetFriendly.SetOwner( player)
+ DispatchSpawn( rightJumpJetFriendly )
+
+ //create left jump jet for enemy
+ entity leftJumpJetEnemy = CreateEntity( "info_particle_system" )
+ leftJumpJetEnemy.kv.start_active = 1
+ leftJumpJetEnemy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY
+ leftJumpJetEnemy.SetValueForEffectNameKey( jumpJetEffectEnemyName )
+ SetTargetName( leftJumpJetEnemy, UniqueString() )
+ leftJumpJetEnemy.SetParent( player, "vent_left_back", false, 0 )
+ SetTeam( leftJumpJetEnemy, playerTeam )
+ leftJumpJetEnemy.SetOwner( player)
+ DispatchSpawn( leftJumpJetEnemy )
+
+ //now create right jump jet for enemy
+ entity rightJumpJetEnemy = CreateEntity( "info_particle_system" )
+ rightJumpJetEnemy.kv.start_active = 1
+ rightJumpJetEnemy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY
+ rightJumpJetEnemy.SetValueForEffectNameKey( jumpJetEffectEnemyName )
+ SetTargetName( rightJumpJetEnemy, UniqueString() )
+ rightJumpJetEnemy.SetParent( player, "vent_right_back", false, 0 )
+ SetTeam( rightJumpJetEnemy, playerTeam )
+ rightJumpJetEnemy.SetOwner( player)
+ DispatchSpawn( rightJumpJetEnemy )
+
+ //sparks from the hand
+ entity handSparks = CreateEntity( "info_particle_system" )
+ handSparks.kv.start_active = 1
+ handSparks.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY
+ handSparks.SetValueForEffectNameKey( $"P_Zipline_hld_1" )
+ SetTargetName( handSparks, UniqueString() )
+ handSparks.SetParent( player, "L_HAND", false, 0 )
+ handSparks.SetOwner( player)
+ DispatchSpawn( handSparks )
+
+ //Do it again for greater intensity!
+ entity handSparks2 = CreateEntity( "info_particle_system" )
+ handSparks2.kv.start_active = 1
+ handSparks2.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY
+ handSparks2.SetValueForEffectNameKey( $"P_Zipline_hld_1" )
+ SetTargetName( handSparks2, UniqueString() )
+ handSparks2.SetParent( player, "L_HAND", false, 0 )
+ handSparks2.SetOwner( player)
+ DispatchSpawn( handSparks2 )
+
+ player.s.ziplineEffects.append( leftJumpJetFriendly )
+ player.s.ziplineEffects.append( rightJumpJetFriendly )
+ player.s.ziplineEffects.append( leftJumpJetEnemy )
+ player.s.ziplineEffects.append( rightJumpJetEnemy )
+
+ player.s.ziplineEffects.append( handSparks )
+ player.s.ziplineEffects.append( handSparks2 )
+}
+
+void function CodeCallback_ZiplineMount( entity player, entity zipline )
+{
+ // printl( "Mounting zipline")
+ #if SERVER
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "player_zipline_attach", "3p_zipline_attach", player, player )
+ #endif
+
+}
+
+void function CodeCallback_ZiplineStart( entity player, entity zipline )
+{
+ #if SERVER
+ CreateZiplineJetEffects( player )
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "player_zipline_loop", "3p_zipline_loop", player, player )
+ foreach ( callback in _ZiplineStartCallbacks )
+ thread callback( player, zipline )
+ #endif
+}
+
+void function CodeCallback_ZiplineMove( entity player, entity zipline )
+{
+ #if SERVER
+ if ( player.IsPhaseShifted() )
+ {
+ foreach( effect in player.s.ziplineEffects )
+ {
+ IsValid( effect )
+ effect.Destroy()
+ }
+ player.s.ziplineEffects.clear()
+ }
+ else if ( player.s.ziplineEffects.len() <= 0 )
+ {
+ CreateZiplineJetEffects( player );
+ }
+ #endif
+}
+
+void function CodeCallback_ZiplineStop( entity player )
+{
+ #if SERVER
+ foreach( effect in player.s.ziplineEffects )
+ {
+ IsValid( effect )
+ effect.Destroy()
+ }
+ player.s.ziplineEffects.clear()
+
+ StopSoundOnEntity( player, "player_zipline_loop" )
+ StopSoundOnEntity( player, "3p_zipline_loop" )
+
+ EmitDifferentSoundsOnEntityForPlayerAndWorld( "player_zipline_detach", "3p_zipline_detach", player, player )
+
+ foreach ( callback in _ZiplineStopCallbacks )
+ thread callback( player )
+ #endif
+}
+
+void function AddCallback_ZiplineStart( void functionref(entity,entity) callback )
+{
+ _ZiplineStartCallbacks.append( callback )
+}
+
+void function AddCallback_ZiplineStop( void functionref(entity) callback )
+{
+ _ZiplineStopCallbacks.append( callback )
+}
+
+function ZiplineMover( entity ent, end, timeTotal, blendIn = 0, blendOut = 0 )
+{
+ Assert( !IsThreadTop(), "This should not be waitthreaded off, it creates timing issues." )
+ entity mover = CreateOwnedScriptMover( ent )
+ ent.SetParent( mover )
+
+ OnThreadEnd(
+ function() : ( ent, mover )
+ {
+ if ( IsValid( mover ) )
+ mover.Destroy()
+ }
+ )
+
+ mover.MoveTo( end, timeTotal, blendIn, blendOut )
+ wait timeTotal
+
+ if ( IsValid( ent ) )
+ ent.ClearParent()
+}
+
+array<string> function GetZiplineLandingAnims()
+{
+ return file.zipLineLandingAnimations
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/pilot/class_wallrun.gnut b/Northstar.CustomServers/mod/scripts/vscripts/pilot/class_wallrun.gnut
new file mode 100644
index 00000000..58de59c1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/pilot/class_wallrun.gnut
@@ -0,0 +1,224 @@
+untyped
+
+global function ClassWallrun_Init
+
+global function Wallrun_AddPlayer
+global function Wallrun_OnPlayerSpawn
+global function Wallrun_OnPlayerDeath
+global function Wallrun_PlayerTookDamage
+global function Wallrun_EnforceWeaponDefaults
+global function Wallrun_CreateCopyOfPilotModel
+
+function ClassWallrun_Init()
+{
+
+ // Make weapons less effective when playing at higher difficulty.
+ level.lethalityMods <- {}
+}
+
+function Wallrun_AddPlayer( player )
+{
+ player.playerClassData[level.pilotClass] <- {}
+}
+
+
+function Wallrun_EnforceWeaponDefaults( player )
+{
+ if ( player.playerClassData[ level.pilotClass ].primaryWeapon )
+ {
+ // settings already exist
+ return
+ }
+
+ player.playerClassData[ level.pilotClass ].primaryWeapon = "mp_weapon_r97"
+ player.playerClassData[ level.pilotClass ].secondaryWeapon = "mp_weapon_sniper"
+
+ local offhandWeaponTable = {}
+ offhandWeaponTable[0] <- { weapon = "mp_weapon_frag_grenade", mods = [] }
+ offhandWeaponTable[1] <- { weapon = "mp_ability_heal", mods = [] }
+
+ player.playerClassData[ level.pilotClass ].offhandWeapons = offhandWeaponTable
+ player.playerClassData[ level.pilotClass ].playerSetFile = DEFAULT_PILOT_SETTINGS
+}
+
+
+function Wallrun_OnPlayerSpawn( player )
+{
+}
+
+
+function Wallrun_PlayerTookDamage( entity player, damageInfo, entity attacker )
+{
+ if ( IsDemigod( player ) )
+ {
+ EntityDemigod_TryAdjustDamageInfo( player, damageInfo )
+ return
+ }
+
+ AdjustDamageForRodeoPlayers( player, damageInfo, attacker )
+
+ #if VERBOSE_DAMAGE_PRINTOUTS
+ printt( " After Wallrun_PlayerTookDamage:", DamageInfo_GetDamage( damageInfo ) )
+ #endif
+
+ if ( player.GetShieldHealthMax() > 0 )
+ {
+ local shieldDamage = PilotShieldHealthUpdate( player, damageInfo )
+ return shieldDamage
+ }
+
+ return
+}
+
+function AdjustDamageForRodeoPlayers( entity player, var damageInfo, entity attacker )
+{
+ if ( player == attacker )
+ return
+
+ local titanSoulRodeoed = player.GetTitanSoulBeingRodeoed()
+ if ( !IsValid( titanSoulRodeoed ) )
+ return
+
+ local playerParent = titanSoulRodeoed.GetTitan()
+
+ // dont let npcs hurt rodeo player
+ if ( attacker.IsNPC() && attacker != playerParent && DamageInfo_GetDamageSourceIdentifier( damageInfo ) != eDamageSourceId.mp_titanability_smoke )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ local damage = DamageInfo_GetDamage( damageInfo )
+
+ if ( !ShouldAdjustDamageForRodeoPlayer( damageInfo ) )
+ return
+
+ local maxPer500ms
+
+ if ( attacker == playerParent )
+ {
+ // rodeo'd player can't damage quite as much
+ maxPer500ms = 56
+ }
+ else
+ if ( playerParent.GetTeam() == player.GetTeam() )
+ {
+ // riding same team titan protects you a bit from random fire on that titan
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_EXPLOSION )
+ {
+ maxPer500ms = 75
+ }
+ else if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_MELEE ) //If melee, players still die in one hit
+ {
+ maxPer500ms = player.GetMaxHealth() + 1
+ }
+ else
+ {
+ maxPer500ms = 175
+ }
+ }
+ else
+ {
+ return
+ }
+
+ //Set a cap on how much damage the playerParent can do.
+ local damageTaken = GetTotalDamageTakenInTime( player, 0.5 )
+
+ local allowedDamage = maxPer500ms - damageTaken
+ if ( damage < allowedDamage )
+ return
+
+ damage = allowedDamage
+ if ( damage <= 0 )
+ damage = 0
+
+ DamageInfo_SetDamage( damageInfo, damage )
+}
+
+
+function ShouldAdjustDamageForRodeoPlayer( damageInfo )
+{
+ int sourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ switch( sourceID )
+ {
+ case eDamageSourceId.rodeo_trap:
+ case eDamageSourceId.mp_titanweapon_vortex_shield:
+ case eDamageSourceId.mp_titanweapon_vortex_shield_ion:
+ case eDamageSourceId.mp_titanability_smoke:
+ case eDamageSourceId.mp_weapon_satchel: //added so that rodeoing players are no longer invulnerable to their satchels when detonated by Titan's smoke
+ return false
+
+ default:
+ return true
+ }
+}
+
+
+function Wallrun_OnPlayerDeath( entity player, damageInfo )
+{
+ if ( IsValidHeadShot( damageInfo, player ) )
+ {
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ local soundAlias
+ if ( damageType & DF_SHOTGUN )
+ {
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Flesh.Shotgun.BulletImpact_Headshot_3P_vs_1P" )
+ soundAlias = "Flesh.Shotgun.BulletImpact_Headshot_3P_vs_3P"
+ }
+ else if ( damageType & damageTypes.bullet || damageType & DF_BULLET )
+ {
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Flesh.Light.BulletImpact_Headshot_3P_vs_1P" )
+ soundAlias = "Flesh.Light.BulletImpact_Headshot_3P_vs_3P"
+ }
+ else if ( damageType & damageTypes.largeCaliber || damageType & DF_GIB )
+ {
+ EmitSoundOnEntityOnlyToPlayer( player, player, "Flesh.Heavy.BulletImpact_Headshot_3P_vs_1P" )
+ soundAlias = "Flesh.Heavy.BulletImpact_Headshot_3P_vs_3P"
+ }
+
+ if ( soundAlias )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ array<entity> pilotArray = GetPlayerArray()
+ //Iterating because we need to not play this sound on 2 pilots and the function only allows for 1. Performance difference is negligible according to Eric M between this and adding a specific code function.
+ foreach ( pilot in pilotArray )
+ {
+ if ( !IsValid( pilot ) )
+ continue
+
+ if ( pilot == player || pilot == attacker )
+ continue
+
+ EmitSoundOnEntityOnlyToPlayer( player, pilot, soundAlias )
+ }
+ }
+ }
+}
+
+
+entity function Wallrun_CreateCopyOfPilotModel( entity player )
+{
+ const string PLAYER_SETTINGS_FIELD = "bodymodel"
+
+ asset modelName
+ if ( player.IsTitan() )
+ {
+ modelName = GetPlayerSettingsAssetForClassName( player.s.storedPlayerSettings, PLAYER_SETTINGS_FIELD )
+ }
+ else
+ {
+ modelName = player.GetPlayerSettingsAsset( PLAYER_SETTINGS_FIELD )
+ }
+
+ entity model = CreatePropDynamic( modelName )
+
+ SetTeam( model, player.GetTeam() )
+
+ //model.SetSkin( 0 )
+
+ RandomizeHead( model )
+
+ return model
+}