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 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 teamLeechedEnts = GetTeamLeechedEnts( player.GetTeam() ) array 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 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" ) }