diff options
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/pilot')
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 +} |