diff options
author | BobTheBob <32057864+BobTheBob9@users.noreply.github.com> | 2021-08-31 23:14:58 +0100 |
---|---|---|
committer | BobTheBob <32057864+BobTheBob9@users.noreply.github.com> | 2021-08-31 23:14:58 +0100 |
commit | 9a96d0bff56f1969c68bb52a2f33296095bdc67d (patch) | |
tree | 4175928e488632705692e3cccafa1a38dd854615 /Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut | |
parent | 27bd240871b7c0f2f49fef137718b2e3c208e3b4 (diff) | |
download | NorthstarMods-9a96d0bff56f1969c68bb52a2f33296095bdc67d.tar.gz NorthstarMods-9a96d0bff56f1969c68bb52a2f33296095bdc67d.zip |
move to new mod format
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut')
-rw-r--r-- | Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut | 1388 |
1 files changed, 1388 insertions, 0 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut new file mode 100644 index 000000000..c0d56de73 --- /dev/null +++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut @@ -0,0 +1,1388 @@ +untyped + +global function AiDrone_Init + +global function CreateDroneSquadString +global function SetDroneSquadStringForOwner +global function GetDroneSquadStringFromOwner +global function DroneGruntThink +global function RunDroneTypeThink +global function DroneHasNoOwner +global function CreateSingleDroneRope +global function DroneDialogue +global function IsDroneRebooting +global function DroneOnLeeched +global function SetRepairDroneTarget + +global const DRONE_SHIELD_COOLDOWN = 8 +global const DRONE_SHIELD_WALL_HEALTH = 200 +global const DRONE_SHIELD_WALL_RADIUS_TITAN = 200 +global const DRONE_SHIELD_WALL_RADIUS_HUMAN = 90 +global const DRONE_SHIELD_WALL_HEIGHT_TITAN = 450 +global const DRONE_SHIELD_WALL_HEIGHT_HUMAN = 190 +global const DRONE_SHIELD_WALL_FOV_TITAN = 115 +global const DRONE_SHIELD_WALL_FOV_HUMAN = 105 +global const DRONE_MINIMUM_DEPLOY_CLEARANCE_FROM_GROUND = 120 +global const MIN_DRONE_SHIELD_FROM_OWNER_DIST = 256 //if shield drone gets more than this distance away from host, will drop shield +global const MIN_DRONE_SHIELD_FROM_OWNER_DIST_TITAN = 400 //if shield drone gets more than this distance away from host, will drop shield +global const DRONE_LEASH_DISTANCE_SQR = 589824 // Further than this distance, drones will disengage from combat and go back to their owner. + +global const SOUND_DRONE_EXPLODE_DEFAULT = "Drone_DeathExplo" +global const SOUND_DRONE_EXPLODE_CLOAK = "Drone_DeathExplo" + +global const FX_DRONE_SHIELD_WALL_TITAN = $"P_drone_shield_wall_XO" +const FX_DRONE_SHIELD_WALL_HUMAN = $"P_drone_shield_wall" +global const FX_DRONE_EXPLOSION = $"P_drone_exp_md" +global const FX_DRONE_R_EXPLOSION = $"P_drone_exp_rocket" +global const FX_DRONE_P_EXPLOSION = $"P_drone_exp_plasma" +global const FX_DRONE_W_EXPLOSION = $"P_drone_exp_worker" +global const FX_DRONE_SHIELD_ROPE_GLOW = $"acl_light_white" + +function AiDrone_Init() +{ + PrecacheParticleSystem( FX_DRONE_EXPLOSION ) + PrecacheParticleSystem( FX_DRONE_R_EXPLOSION ) + PrecacheParticleSystem( FX_DRONE_P_EXPLOSION ) + PrecacheParticleSystem( FX_DRONE_W_EXPLOSION ) + PrecacheParticleSystem( FX_DRONE_SHIELD_WALL_TITAN ) + PrecacheParticleSystem( FX_DRONE_SHIELD_WALL_HUMAN ) + PrecacheParticleSystem( FX_DRONE_SHIELD_ROPE_GLOW ) + + PrecacheModel( $"models/robots/drone_air_attack/drone_air_attack_rockets.mdl" ) + PrecacheModel( $"models/robots/drone_air_attack/drone_air_attack_plasma.mdl" ) + + PrecacheMaterial( $"cable/cable_selfillum.vmt" ) + PrecacheModel( $"cable/cable_selfillum.vmt" ) + AddDeathCallback( "npc_drone", DroneDeath ) +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Fallback behavior if we can't find a valid owner for an orphan Drone +function DroneHasNoOwner( entity drone ) +{ + switch ( GetDroneType( drone ) ) + { + case "drone_type_shield": + //Transform into a Rocket drone and find some buddies + thread DroneTransformsToRocketClass( drone ) + break + + case "drone_type_engineer_combat": + case "drone_type_engineer_shield": + EngineerDroneHasNoOwner( drone ) + break + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void function DroneTransformsToRocketClass( entity drone ) +{ + if ( !IsAlive( drone ) ) + return + + drone.EndSignal( "OnDeath" ) + drone.EndSignal( "OnDestroy" ) + + wait 1.5 + + // dont do it if we're parented for some reason + if ( IsValid( drone.GetParent() ) ) + return + + DroneDialogue( drone, "transform_shield_to_assault" ) + wait 3 + + // dont do it if we're parented for some reason + if ( IsValid( drone.GetParent() ) ) + return + + int team = drone.GetTeam() + int health = drone.GetHealth() + vector origin = drone.GetOrigin() + vector angles = drone.GetAngles() + angles.x = 0 + angles.z = 0 + + entity newDrone = CreateRocketDrone( team, origin, angles ) + DispatchSpawn( newDrone ) + newDrone.SetHealth( health ) + + entity enemy = drone.GetEnemy() + if ( IsAlive( enemy ) ) + newDrone.SetEnemyLKP( enemy, enemy.GetOrigin() ) + + drone.TransferChildrenTo( newDrone ) + + drone.Destroy() + +} + +function EngineerDroneHasNoOwner( drone ) +{ + //TODO: Should probably protect nearest ally, and return to Engineer when he gets close. +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Change drone type on spawn or during gameplay (may transform from one to the other eventually) +function RunDroneTypeThink( drone ) +{ + expect entity( drone ) + #if DEV + Assert( !( "RunDroneTypeThink" in drone.s ), "Already ran drone think!" ) + drone.s.RunDroneTypeThink <- true + #endif + ////initialize it's type only after the anim is complete + //local delay = drone.GetSequenceDuration( spawnAnimDrone ) + drone.EndSignal( "OnDeath" ) + + switch ( GetDroneType( drone ) ) + { + case "drone_type_beam": + case "drone_type_rocket": + case "drone_type_plasma": + local owner = drone.GetFollowTarget() + if ( IsValid( owner ) ) + owner.Signal( "OnEndFollow" ) + DroneRocketThink( drone ) //may delay if it's waiting for a spawn anim to finish + break + + case "drone_type_shield": + DroneShieldThink( drone ) //may delay if it's waiting for a spawn anim to finish + break + + case "drone_type_engineer_combat": + EngineerCombatDroneThink( drone ) //may delay if it's waiting for a spawn anim to finish + break + + case "drone_type_engineer_shield": + EngineerShieldDroneThink( drone ) //may delay if it's waiting for a spawn anim to finish + break + + case "drone_type_repair": + RepairDroneThink( drone ) + break + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function DroneRocketThink( entity drone ) +{ + drone.EndSignal( "OnDeath" ) + + entity owner + entity currentTarget + local accuracyMultiplierBase = drone.kv.AccuracyMultiplier + local accuracyMultiplierAgainstDrones = 100 + + //-------------------------------------------- + // transform if this used to be a shield drone + //-------------------------------------------- + RemoveDroneRopes( drone ) + drone.SetAttackMode( true ) + + while ( true ) + { + wait 0.25 + + //---------------------------------- + // Get owner and current enemy + //---------------------------------- + currentTarget = drone.GetEnemy() + owner = drone.GetFollowTarget() + + //---------------------------------- + // Free roam if owner is dead or HasEnemy + //---------------------------------- + if ( ( !IsAlive( owner ) ) || ( currentTarget != null ) ) + { + drone.DisableBehavior( "Follow" ) + } + + //--------------------------------------------------------------------- + // If owner is alive and no enemies in sight, go back and follow owner + //---------------------------------------------------------------------- + if ( IsAlive( owner ) ) + { + local distSqr = DistanceSqr( owner.GetOrigin(), drone.GetOrigin() ) + + if ( currentTarget == null || distSqr > DRONE_LEASH_DISTANCE_SQR ) + { + drone.ClearEnemy() + drone.EnableBehavior( "Follow" ) + } + } + + //---------------------------------------------- + // Jack up accuracy if targeting another drone + //---------------------------------------------- + if ( ( currentTarget != null ) && ( IsAirDrone( currentTarget ) ) ) + { + drone.kv.AccuracyMultiplier = accuracyMultiplierAgainstDrones + } + else + { + drone.kv.AccuracyMultiplier = accuracyMultiplierBase + } + } + +} + +function ShieldDroneShieldsUser( entity drone ) +{ + for ( ;; ) + { + var player = drone.WaitSignal( "OnPlayerUse" ).player + + Assert( false, "REMOVED; see mp_pilot_ability_shield to ressurect" ) + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function DroneShieldThink( drone ) +{ + expect entity( drone ) + if ( !IsValid( drone ) ) + return + drone.EndSignal( "OnDestroy" ) + drone.EndSignal( "OnDeath" ) + //drone.EndSignal( "OnNewOwner" ) + + entity owner + local newOwner + string ownerSquadName = "" + local distSq + local distSqHuman = MIN_DRONE_SHIELD_FROM_OWNER_DIST * MIN_DRONE_SHIELD_FROM_OWNER_DIST + local distSqTitan = MIN_DRONE_SHIELD_FROM_OWNER_DIST_TITAN * MIN_DRONE_SHIELD_FROM_OWNER_DIST_TITAN + bool titanStateCurrent = false + bool titanStatePrevious = false + bool titanStateChanged = false + local e = {} + e.droneShieldTable <- null + + drone.SetUsePrompts( "#SHIELD_DRONE_HOLD_USE", "#SHIELD_DRONE_PRESS_USE" ) + + //------------------------------------------ + // Cleanup shield if Drone dies + //------------------------------------------ + OnThreadEnd( + function() : ( e, drone ) + { + DroneShieldDestroy( e.droneShieldTable ) + if ( IsAlive( drone ) ) + thread ShieldDroneLandsAfterLeaderDeath( drone ) + } + ) + + thread ShieldDroneShieldsUser( drone ) + + //------------------------------------------ + // Drone tentacles/ropes + //------------------------------------------ + local droneRopeTable = CreateDroneRopes( drone ) + if ( !( "droneRopeTable" in drone.s ) ) + drone.s.droneRopeTable <- null + drone.s.droneRopeTable = droneRopeTable + + //------------------------------------------ + // Drone shield think loop + //------------------------------------------ + + while ( true ) + { + wait 0.25 + + if ( GetDroneType( drone ) != "drone_type_shield" ) + { + DroneShieldDestroy( e.droneShieldTable ) + break + } + + //------------------------------------------ + // If rebooting from EMP blast, get rid of shield + //------------------------------------------ + if ( IsDroneRebooting( drone ) ) + { + DroneShieldDestroy( e.droneShieldTable ) + continue + } + //------------------------------------------ + // If owner dead, kill shield until new owner found + //------------------------------------------ + owner = drone.GetFollowTarget() + if ( !IsAlive( owner ) ) + { + DroneShieldDestroy( e.droneShieldTable ) + break + } + + //------------------------------------------ + // Still no valid owner? End this thread + //------------------------------------------ + if ( !IsValid( owner ) ) + break + + //ownerSquadName = owner.Get( "squadname" ) + + //------------------------------------------ + // Owner is valid. Is it differnt owner? + //------------------------------------------ + if ( newOwner != owner ) + { + //Kill current shield since it will get redeployed on new owner + DroneShieldDestroy( e.droneShieldTable ) + } + + //------------------------------------------ + // Owner is valid. Has owner changed Titan state? + //------------------------------------------ + newOwner = owner + titanStatePrevious = titanStateCurrent //previous state is whatever current was set to last loop around + + if ( owner.IsTitan() ) + { + distSq = distSqTitan //adjust min dist for shield based on titan state + titanStateCurrent = true //toggle so we can see if owner just changed state + } + else + { + distSq = distSqHuman + titanStateCurrent = false + } + + if ( titanStateCurrent != titanStatePrevious ) + titanStateChanged = true + else + titanStateChanged = false + + //-------------------------------------------------------------------------------------- + // We have a valid owner and a valid shield, continue unless we have changed Titan state + //-------------------------------------------------------------------------------------- + if ( ( DroneShieldExists( e.droneShieldTable ) ) && ( !titanStateChanged ) ) + continue + + //------------------------------------------ + // Too far away from owner, destoy shield + //------------------------------------------ + if ( DistanceSqr( drone.GetOrigin(), owner.GetOrigin() ) > distSq ) + { + //printl( "Drone is too far away from host to create a shield") + DroneShieldDestroy( e.droneShieldTable ) + continue + } + + //------------------------------------------ + // Owner embarked/disembarked in a Titan, destroy shield + //------------------------------------------ + if ( titanStateChanged ) + { + //printl( "Drone host embarked/disembarked a Titan, destroying shield") + DroneShieldDestroy( e.droneShieldTable ) + continue + } + //---------------------------------------------------------- + // Valid owner, valid dist, etc...make a shield for the current owner + //----------------------------------------------------------- + e.droneShieldTable = MakeDroneShield( drone, owner ) + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function EngineerCombatDroneThink( entity drone ) +{ + if ( !IsValid( drone ) ) + return + + drone.EndSignal( "OnDeath" ) + + entity owner + local currentTarget + local accuracyMultiplierPlayers = 50 + local accuracyMultiplierAgainstNPC = 90 + + //-------------------------------------------- + // transform if this used to be a shield drone + //-------------------------------------------- + RemoveDroneRopes( drone ) + drone.SetAttackMode( true ) + + while ( true ) + { + wait 0.25 + + //---------------------------------- + // Get owner and current enemy + //---------------------------------- + currentTarget = drone.GetEnemy() + owner = drone.GetFollowTarget() + + //---------------------------------- + // Free roam if owner is dead or HasEnemy + //---------------------------------- + if ( ( !IsAlive( owner ) ) || ( currentTarget != null ) ) + { + drone.DisableBehavior( "Follow" ) + } + + //--------------------------------------------------------------------- + // If owner is alive and no enemies in sight, go back and follow owner + //---------------------------------------------------------------------- + if ( IsAlive( owner ) ) + { + float distSqr = DistanceSqr( owner.GetOrigin(), drone.GetOrigin() ) + + if ( currentTarget == null || distSqr > DRONE_LEASH_DISTANCE_SQR ) + { + drone.ClearEnemy() + drone.EnableBehavior( "Follow" ) + } + } + + //---------------------------------------------- + // Jack up accuracy if targeting another drone + //---------------------------------------------- + if ( ( currentTarget != null ) && ( currentTarget.IsNPC() ) ) + { + drone.kv.AccuracyMultiplier = accuracyMultiplierAgainstNPC + } + else + { + drone.kv.AccuracyMultiplier = accuracyMultiplierPlayers + } + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function EngineerShieldDroneThink( drone ) +{ + if ( !IsValid( drone ) ) + return + drone.EndSignal( "OnDestroy" ) + drone.EndSignal( "OnDeath" ) +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function IsDroneRebooting( drone ) +{ + if ( !( "rebooting" in drone.s ) ) + return false + + return drone.s.rebooting +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// HACK: may just use generic function "CreateShield()" from particle_wall.nut, but just in prototype mode now +function MakeDroneShield( drone, owner ) +{ + expect entity( owner ) + + if ( !( "shieldTable" in drone.s ) ) + drone.s.shieldTable <- null + else + DroneShieldDestroy( drone.s.shieldTable ) + + //------------------------------ + // Shield vars + //------------------------------ + vector origin = owner.GetOrigin() + vector angles = owner.GetAngles() + Vector( 0, 0, 180 ) + local attachmentTag + local DroneShieldTable = {} + DroneShieldTable.vortexSphere <- null + DroneShieldTable.shieldWallFX = null + DroneShieldTable.shieldRopes <- null + + asset shieldFx + float wallFOV + float shieldWallRadius + float shieldWallHeight + if ( owner.IsTitan() ) + { + shieldWallRadius = DRONE_SHIELD_WALL_RADIUS_TITAN + shieldFx = FX_DRONE_SHIELD_WALL_TITAN + wallFOV = DRONE_SHIELD_WALL_FOV_TITAN + shieldWallHeight = DRONE_SHIELD_WALL_HEIGHT_TITAN + } + else + { + shieldWallRadius = DRONE_SHIELD_WALL_RADIUS_HUMAN + shieldFx = FX_DRONE_SHIELD_WALL_HUMAN + wallFOV = DRONE_SHIELD_WALL_FOV_HUMAN + shieldWallHeight = DRONE_SHIELD_WALL_HEIGHT_HUMAN + } + + local Spawn + //------------------------------ + // Vortex to block the actual bullets + //------------------------------ + entity vortexSphere = CreateEntity( "vortex_sphere" ) + + vortexSphere.kv.spawnflags = SF_ABSORB_BULLETS | SF_BLOCK_OWNER_WEAPON | SF_BLOCK_NPC_WEAPON_LOF | SF_ABSORB_CYLINDER + vortexSphere.kv.enabled = 0 + vortexSphere.kv.radius = shieldWallRadius + vortexSphere.kv.height = shieldWallHeight + vortexSphere.kv.bullet_fov = wallFOV + vortexSphere.kv.physics_pull_strength = 25 + vortexSphere.kv.physics_side_dampening = 6 + vortexSphere.kv.physics_fov = 360 + vortexSphere.kv.physics_max_mass = 2 + vortexSphere.kv.physics_max_size = 6 + + vortexSphere.SetAngles( angles ) // viewvec? + vortexSphere.SetOrigin( origin + Vector( 0, 0, shieldWallRadius - 64 ) ) + vortexSphere.SetMaxHealth( DRONE_SHIELD_WALL_HEALTH ) + vortexSphere.SetHealth( DRONE_SHIELD_WALL_HEALTH ) + + if ( IsSingleplayer() ) + { + thread PROTO_VortexSlowsPlayers( vortexSphere, owner ) + } + + DispatchSpawn( vortexSphere ) + + vortexSphere.Fire( "Enable" ) + + vortexSphere.SetInvulnerable() // make particle wall invulnerable to weapon damage. It will still drain over time + + // Shield wall fx control point + entity cpoint = CreateEntity( "info_placement_helper" ) + SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) ) + DispatchSpawn( cpoint ) + + //------------------------------------------ + // Shield wall fx for visuals/health drain + //------------------------------------------ + + entity shieldWallFX = PlayFXWithControlPoint( shieldFx, origin + Vector( 0, 0, -64 ), cpoint, -1, null, angles ) + vortexSphere.e.shieldWallFX = shieldWallFX + SetVortexSphereShieldWallCPoint( vortexSphere, cpoint ) + + entity mover = CreateScriptMover() + mover.SetOrigin( owner.GetOrigin() ) + mover.SetAngles( owner.GetAngles() ) + + //----------------------- + // Attach shield to owner + //------------------------ + vortexSphere.SetParent( mover ) + shieldWallFX.SetParent( mover ) + + thread ShieldMoverFollowsOwner( owner, mover, vortexSphere, shieldWallFX ) + + //----------------------- + // Rope attach to shield + //------------------------ + local ropeAttachOrigin1 = PositionOffsetFromEnt( owner, shieldWallRadius -16, wallFOV -16, 128 ) + local ropeAttachOrigin2 = PositionOffsetFromEnt( owner, shieldWallRadius -16, ( ( wallFOV - 16) * -1 ), 128 ) + if ( owner.IsTitan() ) + { + ropeAttachOrigin1 = PositionOffsetFromEnt( owner, shieldWallRadius - 78, wallFOV + 22, 256 ) + ropeAttachOrigin2 = PositionOffsetFromEnt( owner, shieldWallRadius - 78, -( wallFOV + 22), 256 ) + } + + local shieldRopes = [] + local shieldRope1 = CreateSingleDroneRope( drone, "ROPE_0", false ) + local shieldRope2 = CreateSingleDroneRope( drone, "ROPE_0", false ) + shieldRopes.append( shieldRope1 ) + shieldRopes.append( shieldRope2 ) + entity ropeEnt1 = CreateEntity( "info_target" ) + entity ropeEnt2 = CreateEntity( "info_target" ) + ropeEnt1.SetOrigin( ropeAttachOrigin1 ) + ropeEnt2.SetOrigin( ropeAttachOrigin2 ) + ropeEnt1.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT + ropeEnt2.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT + DispatchSpawn( ropeEnt1 ) + DispatchSpawn( ropeEnt2 ) + + ropeEnt1.SetParent( vortexSphere ) + ropeEnt2.SetParent( vortexSphere ) + shieldRope1.s.ropeEnd.SetOrigin( ropeEnt1.GetOrigin() ) + shieldRope2.s.ropeEnd.SetOrigin( ropeEnt2.GetOrigin() ) + shieldRope1.s.ropeEnd.SetParent( ropeEnt1 ) + shieldRope2.s.ropeEnd.SetParent( ropeEnt2 ) + + PlayFXOnEntity( FX_DRONE_SHIELD_ROPE_GLOW, ropeEnt1 ) + PlayFXOnEntity( FX_DRONE_SHIELD_ROPE_GLOW, ropeEnt2 ) + + //----------------------- + // DroneShieldTable + //------------------------ + DroneShieldTable.vortexSphere = vortexSphere + DroneShieldTable.shieldWallFX = shieldWallFX + DroneShieldTable.shieldRopes = shieldRopes + + //----------------------- + // Health and cleanup + //------------------------ + drone.s.shieldTable = DroneShieldTable + UpdateShieldWallColorForFrac( shieldWallFX, 1.0 ) + + return DroneShieldTable +} + +void function ShieldMoverFollowsOwner( entity owner, entity mover, entity vortexSphere, entity shieldWallFX ) +{ + vortexSphere.EndSignal( "OnDestroy" ) + shieldWallFX.EndSignal( "OnDestroy" ) + owner.EndSignal( "OnDeath" ) + mover.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( mover ) + { + if ( IsValid( mover ) ) + mover.Destroy() + } + ) + + for ( ;; ) + { + UpdateMoverPosition( mover, owner ) + } +} + +void function UpdateMoverPosition( entity mover, entity owner ) +{ + vector origin = owner.GetOrigin() + mover.NonPhysicsMoveTo( origin, 0.1, 0.0, 0.0 ) + mover.NonPhysicsRotateTo( owner.GetAngles(), 0.75, 0.0, 0.0 ) + WaitFrame() +} + +void function PROTO_VortexSlowsPlayers( entity vortexSphere, entity owner ) +{ + vortexSphere.EndSignal( "OnDestroy" ) + owner.EndSignal( "OnDeath" ) + + float radius = float(vortexSphere.kv.radius ) + float height = float(vortexSphere.kv.height ) + float bullet_fov = float( vortexSphere.kv.bullet_fov ) + float dot = cos( bullet_fov * 0.5 ) + + for ( ;; ) + { + vector origin = vortexSphere.GetOrigin() + vector angles = vortexSphere.GetAngles() + vector forward = AnglesToForward( angles ) + int team = owner.GetTeam() + + foreach ( player in GetPlayerArray() ) + { + if ( player.GetTeam() == team ) + continue + VortexStunCheck( player, origin, height, radius, bullet_fov, dot, forward ) + } + WaitFrame() + } +} + +void function VortexStunCheck( entity player, vector origin, float height, float radius, float bullet_fov, float dot, vector forward ) +{ + if ( Time() - player.p.lastDroneShieldStunPushTime < 1.75 ) + return + + vector playerOrg = player.GetOrigin() + float dist2d = Distance2D( playerOrg, origin ) + + if ( dist2d > radius + 5 ) + return + if ( dist2d < radius - 15 ) + return + + float heightOffset = fabs( playerOrg.z - origin.z ) + + if ( heightOffset < 0 || heightOffset > height ) + return + + vector dif = Normalize( playerOrg - origin ) + + if ( DotProduct2D( dif, forward ) < dot ) + return + + const float VORTEX_STUN_DURATION = 1.0 + GiveEMPStunStatusEffects( player, VORTEX_STUN_DURATION + 0.5 ) + float strength = 0.4 + StatusEffect_AddTimed( player, eStatusEffect.emp, strength, VORTEX_STUN_DURATION, 0.5 ) + thread TempLossOfAirControl( player, VORTEX_STUN_DURATION ) + vector velocity = forward * 300 + velocity.z = 400 + player.p.lastDroneShieldStunPushTime = Time() + + EmitSoundOnEntityOnlyToPlayer( player, player, "explo_proximityemp_impact_3p" ) + player.SetVelocity( velocity ) +} +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function CreateDroneRopes( drone ) +{ + local droneRopeTable = {} + droneRopeTable.rope01 <- CreateSingleDroneRope( drone, "ROPE_0" ) + droneRopeTable.rope02 <- CreateSingleDroneRope( drone, "ROPE_0" ) + droneRopeTable.rope03 <- CreateSingleDroneRope( drone, "ROPE_1" ) + droneRopeTable.rope04 <- CreateSingleDroneRope( drone, "ROPE_2" ) + droneRopeTable.rope05 <- CreateSingleDroneRope( drone, "ROPE_3" ) + droneRopeTable.rope06 <- CreateSingleDroneRope( drone, "ROPE_4" ) + + return droneRopeTable +} +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function RemoveDroneRopes( entity drone ) +{ + if ( !( "droneRopeTable" in drone.s ) ) + return + + local droneRopeTable = drone.s.droneRopeTable + if ( IsValid( droneRopeTable.rope01.s.ropeEnd ) ) + droneRopeTable.rope01.s.ropeEnd.Destroy() + if ( IsValid( droneRopeTable.rope02.s.ropeEnd ) ) + droneRopeTable.rope02.s.ropeEnd.Destroy() + if ( IsValid( droneRopeTable.rope03.s.ropeEnd ) ) + droneRopeTable.rope03.s.ropeEnd.Destroy() + if ( IsValid( droneRopeTable.rope04.s.ropeEnd ) ) + droneRopeTable.rope04.s.ropeEnd.Destroy() + if ( IsValid( droneRopeTable.rope05.s.ropeEnd ) ) + droneRopeTable.rope05.s.ropeEnd.Destroy() + if ( IsValid( droneRopeTable.rope06.s.ropeEnd ) ) + droneRopeTable.rope06.s.ropeEnd.Destroy() + if ( IsValid( droneRopeTable.rope01 ) ) + droneRopeTable.rope01.Destroy() + if ( IsValid( droneRopeTable.rope02 ) ) + droneRopeTable.rope02.Destroy() + if ( IsValid( droneRopeTable.rope03 ) ) + droneRopeTable.rope03.Destroy() + if ( IsValid( droneRopeTable.rope04 ) ) + droneRopeTable.rope04.Destroy() + if ( IsValid( droneRopeTable.rope05 ) ) + droneRopeTable.rope05.Destroy() + if ( IsValid( droneRopeTable.rope06 ) ) + droneRopeTable.rope06.Destroy() + +} +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function CreateSingleDroneRope( drone, attachTag, dangling = true ) +{ + local subdivisions = 15 // 25 + local slack = 200 // 25 + string startpointName = UniqueString( "rope_startpoint" ) + string endpointName = UniqueString( "rope_endpoint" ) + + local attach_id = drone.LookupAttachment( attachTag ) + Assert( attach_id > 0, "Invalid attachment: " + attachTag ) + local attachPos = drone.GetAttachmentOrigin( attach_id ) + + entity rope_start = CreateEntity( "move_rope" ) + SetTargetName( rope_start, startpointName ) + rope_start.kv.NextKey = endpointName + rope_start.kv.MoveSpeed = 32 + rope_start.kv.Slack = slack + rope_start.kv.Subdiv = subdivisions + rope_start.kv.Width = "1" + rope_start.kv.TextureScale = "1" + rope_start.kv.RopeMaterial = "cable/cable_selfillum.vmt" + rope_start.kv.PositionInterpolator = 2 + rope_start.kv.dangling = dangling + rope_start.SetOrigin( attachPos ) + rope_start.SetParent( drone, attachTag ) + + entity rope_end = CreateEntity( "keyframe_rope" ) + SetTargetName( rope_end, endpointName ) + rope_end.kv.MoveSpeed = 32 + rope_end.kv.Slack = slack + rope_end.kv.Subdiv = subdivisions + rope_end.kv.Width = "1" + rope_end.kv.TextureScale = "1" + rope_end.kv.RopeMaterial = "cable/cable_selfillum.vmt" + rope_end.SetOrigin( attachPos ) + + DispatchSpawn( rope_start ) + DispatchSpawn( rope_end ) + + rope_start.s.ropeEnd <- rope_end + + return rope_start +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function DroneShieldDestroy( DroneShieldTable ) +{ + if ( !IsValid( DroneShieldTable ) ) + return + + local vortexSphere = DroneShieldTable.vortexSphere + local shieldWallFX = DroneShieldTable.shieldWallFX + local ropes = DroneShieldTable.shieldRopes + + StopShieldWallFX( expect entity( vortexSphere ) ) + if ( IsValid( vortexSphere ) ) + vortexSphere.Destroy() + + if ( !IsValid( ropes ) ) + return + + foreach ( rope in ropes ) + { + if ( IsValid( rope.s.ropeEnd ) ) + rope.s.ropeEnd.Destroy() + if ( IsValid( rope ) ) + rope.Destroy() + } + +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function DroneShieldExists( DroneShieldTable ) +{ + if ( !IsValid( DroneShieldTable) ) + return false + + Assert( "vortexSphere" in DroneShieldTable, "DroneShieldTable doesn't contain any valid entries for vortexSphere." ) + Assert( "shieldWallFX" in DroneShieldTable, "DroneShieldTable doesn't contain any valid entries for shieldWallFX." ) + + if ( ( IsValid( DroneShieldTable.vortexSphere ) ) && ( IsValid( DroneShieldTable.shieldWallFX ) ) ) + return true + + return false +} + +void function DroneThrow( entity npc, entity drone, string spawnAnimDrone ) +{ + drone.EndSignal( "OnDeath" ) + + drone.EnableNPCFlag( NPC_DISABLE_SENSING ) + +// EmitSoundOnEntity( drone, "Drone_Power_On" ) + + #if GRUNTCHATTER_ENABLED + if ( NPC_GruntChatterSPEnabled( npc ) ) + GruntChatter_TryFriendlyEquipmentDeployed( npc, "npc_drone" ) + #endif + + vector origin = npc.GetOrigin() + vector angles = npc.GetAngles() + + //animate the drone properly from the npc's hand + PlayAnimTeleport( drone, spawnAnimDrone, origin, angles ) + + if ( IsAlive( npc ) ) + { + entity enemy = npc.GetEnemy() + if ( IsAlive( enemy ) ) + drone.SetEnemyLKP( enemy, npc.GetEnemyLKP() ) + } + + drone.DisableNPCFlag( NPC_DISABLE_SENSING ) +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#if !SP +void function DroneCleanupOnOwnerDeath_Thread( entity owner, entity drone ) +{ + drone.EndSignal( "OnDestroy" ) + drone.EndSignal( "OnDeath" ) + + for ( ; ; ) + { + if ( !IsAlive( owner ) ) + break + + WaitFrame() + } + + wait RandomFloatRange( 2.0, 10.0 ) + drone.Die() +} +#endif // #if !SP + +entity function SpawnDroneFromNPC( entity npc, string aiSettings ) +{ + //he's busy right now + if ( !IsAlive( npc ) || !npc.IsInterruptable() ) + return null + + vector origin = npc.GetOrigin() + vector angles = npc.GetAngles() + int team = npc.GetTeam() + entity owner = npc + vector deployOrigin = PositionOffsetFromEnt( npc, 64, 0, 0 ) + float verticalClearance = GetVerticalClearance( deployOrigin ) + string spawnAnimDrone + string spawnAnimSoldier + + //------------------------------------------------------------------- + // Make sure enough clearance to spawn drone, and get correct anim + //------------------------------------------------------------------- + if ( verticalClearance >= 256 ) + { + spawnAnimDrone = "dr_activate_drone_spin" + spawnAnimSoldier = "pt_activate_drone_spin" + } + else if ( ( verticalClearance < 256 ) && ( verticalClearance > DRONE_MINIMUM_DEPLOY_CLEARANCE_FROM_GROUND ) ) + { + spawnAnimDrone = "dr_activate_drone_indoor" + spawnAnimSoldier = "pt_activate_drone_indoor" + } + else + { + printt( "NPC at ", npc.GetOrigin(), " couldn't spawn drone because there is less than ", DRONE_MINIMUM_DEPLOY_CLEARANCE_FROM_GROUND, " units of clearance from his origin." ) + return null + } + + //------------------------------------------ + // NPC throws drone into air + //------------------------------------------ + entity drone = CreateNPC( "npc_drone", team, origin, angles ) + SetSpawnOption_AISettings( drone, aiSettings ) + DispatchSpawn( drone ) + + if ( !IsAlive( drone ) ) + return null + + drone.NotSolid() + thread PlayAnim( npc, spawnAnimSoldier, origin, angles ) + thread DroneSolidDelayed( drone ) + thread DroneThrow( npc, drone, spawnAnimDrone ) + +#if !SP + thread DroneCleanupOnOwnerDeath_Thread( npc, drone ) +#endif // #if !SP + + npc.EnableNPCFlag( NPC_PAIN_IN_SCRIPTED_ANIM ) + + return drone +} + +void function DroneSolidDelayed( entity drone ) +{ + drone.EndSignal( "OnDestroy" ) + wait 3.0 // wait for custom scale to finish in the animation + drone.Solid() +} + +void function ShieldDroneLandsAfterLeaderDeath( entity drone ) +{ + Assert( IsNewThread(), "Must be threaded off" ) + drone.EndSignal( "OnDeath" ) + + drone.DisableBehavior( "Follow" ) + //SetTeam( drone, TEAM_UNASSIGNED ) + vector start = drone.GetOrigin() + vector end = start + Vector(0,0,-5000) + vector mins = drone.GetBoundingMins() + vector maxs = drone.GetBoundingMaxs() + + TraceResults traceResult = TraceHull( start, end, mins, maxs, null, TRACE_MASK_NPCWORLDSTATIC, TRACE_COLLISION_GROUP_NONE ) + if ( traceResult.fraction >= 1.0 ) + { + // cant touch ground + drone.Die() + return + } + + RemoveDroneRopes( drone ) + + //drone.SetUsable() + drone.AssaultPoint( traceResult.endPos ) + //drone.SetInvulnerable() +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function CreateDroneSquadString( owner ) +{ + Assert( IsValid( owner ), "Trying to MakeDroneSquad name for an invalid entity." ) + + local squadName + + if ( owner.IsPlayer() ) + squadName = "player" + owner.entindex() + "droneSquad" + else if ( owner.IsNPC() ) + squadName = "npc" + owner.entindex() + "droneSquad" + else + Assert( 0, "Trying to CreateDroneSquadString for a non-NPC non-player entity at " + owner.GetOrigin() ) + + return squadName +} +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function SetDroneSquadStringForOwner( owner, squadName ) +{ + Assert( IsValid( owner ), "Trying to SetDroneSquadStringForOwner name on an invalid entity." ) + + if ( !( "squadNameDrones" in owner.s ) ) + owner.s.squadNameDrones <- null + + owner.s.squadNameDrones = squadName +} +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function GetDroneSquadStringFromOwner( owner ) +{ + Assert( IsValid( owner ), "Trying to GetDroneSquadStringFromOwner name on an invalid entity." ) + if ( !( "squadNameDrones" in owner.s ) ) + return null + else + return owner.s.squadNameDrones +} +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// DroneGrunt deploys drone after cooldown when drone is destroyed +function DroneGruntThink( entity npc, string aiSettings ) +{ + if ( !IsValid( npc ) ) + return + + npc.EndSignal( "OnDestroy" ) + npc.EndSignal( "OnDeath" ) + + entity drone + float spawnCooldown + entity closestEnemy + npc.EnableNPCFlag( NPC_USE_SHOOTING_COVER | NPC_CROUCH_COMBAT ) + + while ( true ) + { + //if ( npc.GetNPCState() == "idle" ) + //{ + // npc.WaitSignal( "OnStateChange" ) + // continue + //} + + wait ( RandomFloatRange( 0, 1.0 ) ) + + //dont do stuff when animating on a parent + if ( npc.GetParent() ) + continue + + // Don't deploy if would hit ceiling, droppod, etc + if ( !DroneHasEnoughRoomToDeployFromNPC( npc ) ) + continue + + entity enemy = npc.GetEnemy() + if ( !IsAlive( enemy ) ) + continue + + //vector pos = npc.LastKnownPosition( enemy ) + //if ( !WithinEngagementRange( npc, pos ) ) + // continue + + drone = SpawnDroneFromNPC( npc, aiSettings ) + if ( drone == null ) + continue + + waitthread DroneWaitTillDeadOrHacked( drone ) + + wait 15 + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function DroneHasEnoughRoomToDeployFromNPC( npc ) +{ + expect entity( npc ) + + if ( !IsValid( npc ) ) + return false + //----------------------------------------------- + // Grunt throws drone a bit in front of him + //----------------------------------------------- + vector deployOrigin = PositionOffsetFromEnt( npc, 64, 0, 0 ) + + if ( GetVerticalClearance( deployOrigin ) < DRONE_MINIMUM_DEPLOY_CLEARANCE_FROM_GROUND ) + return false + else + return true + +} +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function DroneWaitTillDeadOrHacked( drone ) +{ + drone.EndSignal( "OnDestroy" ) + drone.EndSignal( "OnDeath" ) + drone.EndSignal( "OnNewOwner" ) + + WaitForever() +} +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void function DroneDeath( entity drone, var damageInfo ) +{ + local deathFX + + switch ( GetDroneType( drone ) ) + { + case "drone_type_rocket": + deathFX = FX_DRONE_R_EXPLOSION + break + case "drone_type_plasma": + deathFX = FX_DRONE_P_EXPLOSION + break + case "drone_type_marvin": + deathFX = FX_DRONE_W_EXPLOSION + break + case "drone_type_shield": + case "drone_type_engineer_shield": + case "drone_type_engineer_combat": + default: + deathFX = FX_DRONE_EXPLOSION + break + } + + // Explosion effect + entity explosion = CreateEntity( "info_particle_system" ) + explosion.SetOrigin( drone.GetWorldSpaceCenter() ) + explosion.SetAngles( drone.GetAngles() ) + explosion.SetValueForEffectNameKey( deathFX ) + explosion.kv.start_active = 1 + DispatchSpawn( explosion ) + + local deathSound + + // this sound get should be moved to ai settings file + switch ( GetDroneType( drone ) ) + { + case "drone_type_rocket": + case "drone_type_plasma": + case "drone_type_marvin": + case "drone_type_shield": + case "drone_type_engineer_shield": + case "drone_type_engineer_combat": + deathSound = SOUND_DRONE_EXPLODE_DEFAULT + break + default: + deathSound = SOUND_DRONE_EXPLODE_DEFAULT + break + } + + EmitSoundAtPosition( TEAM_UNASSIGNED, drone.GetOrigin(), deathSound ) + explosion.Kill_Deprecated_UseDestroyInstead( 3 ) +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +function DroneDialogue( drone, event, player = null ) +{ + expect entity( drone ) + expect entity( player ) + + if ( !IsAlive( drone ) ) + return + + if ( player != null ) + { + if ( !IsAlive( player ) ) + return + } + + local alias + bool playToPlayerOnly = true + + switch ( event ) + { + case "smoke_deploy": + //Foreign entity attached, deploying countermeasures. + alias = "diag_gs_drone_detectEnemyRodeo" + break + case "hack_success": + //New host accepted. + alias = "diag_gs_drone_hostAcceptNew" + + //Foreign host accepted. + if ( CoinFlip() ) + alias = "diag_gs_drone_hostAcceptForeign" + break + case "transform_shield_to_assault": + //Drone host eliminated, engaging assault mode + alias = "diag_gs_drone_elimHost" + playToPlayerOnly = false + break + default: + Assert( 0, "Invalid DroneDialogue event: " + event ) + } + + if ( playToPlayerOnly ) + EmitSoundOnEntityOnlyToPlayer( drone, player, alias ) + else + EmitSoundOnEntity( drone, alias ) + + +/* +Hostiles detected, marking targets +diag_gs_drone_detectHostileTargets + +Drone targets marked +diag_gs_drone_targetsMarked + +Escort drone destroyed +diag_gs_drone_escortDestroyed + +Multiple escort drones combined. Shield radius increased +diag_gs_drone_combinedShieldRadius + +Multiple escort drones combined. Projectile accuracy increased +diag_gs_drone_combinedWpnAccuracy + +Recharging drone shield +diag_gs_drone_rechargingShield + +Target lost +diag_gs_drone_targetLost + +Target acquired +diag_gs_drone_targetAcquired +*/ + +} + + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +function DroneOnLeeched( drone, player ) +{ + //global behavior when this npc gets leeched + delaythread ( 1 ) DroneDialogue( drone, "hack_success", player ) +} + +function DroneSelfDestruct( drone, delay ) +{ + drone.EndSignal( "OnDeath" ) + wait delay + drone.Die() +} + +function RepairDroneThink( entity drone ) +{ + drone.EndSignal( "OnDeath" ) + local attachID + EmitSoundOnEntity( drone, "colony_spectre_initialize_beep" ) + thread DroneSelfDestruct( drone, 60 ) + + for ( ;; ) + { + if ( drone.e.repairSoul == null ) + { + wait 1 + continue + } + + string attachName = "HIJACK" + entity repairTitan = drone.e.repairSoul.GetTitan() + + /* + if ( IsSoul( repairTarget ) ) + { + repairTarget = repairTarget.GetTitan() + attachName = "HIJACK" + } + else + { + Assert( !repairTarget.IsTitan() ) + attachName = "ORIGIN" + } + + if ( !IsAlive( repairTitan ) ) + { + wait 2 + continue + } + */ + + drone.SetOwner( repairTitan ) + + if ( DroneCanRepairTarget( drone, repairTitan, attachName ) ) + { + // close enough to repair? + //P_wpn_defender_beam + waitthread DroneRepairsTarget( drone, repairTitan, attachName ) + } + WaitFrame() + } +} + +bool function DroneCanRepairTarget( drone, ent, attachName ) +{ + expect entity( ent ) + + if ( !IsAlive( ent ) ) + return false + + if ( ent.GetHealth() >= ent.GetMaxHealth() ) + return false + + local attachID = ent.LookupAttachment( attachName ) + local origin = ent.GetAttachmentOrigin( attachID ) + local droneOrigin = drone.GetOrigin() + if ( Distance( droneOrigin, origin ) > 600 ) + return false + + float trace = TraceLineSimple( droneOrigin, origin, ent ) + return trace == 1.0 +} + +function DroneRepairsTarget( drone, ent, attachName ) +{ + expect entity( drone ) + expect entity( ent ) + + drone.EndSignal( "OnDestroy" ) + EmitSoundOnEntity( drone, "EMP_Titan_Electrical_Field" ) + + OnThreadEnd( + function() : ( drone ) + { + if ( IsValid( drone ) ) + StopSoundOnEntity( drone, "EMP_Titan_Electrical_Field" ) + } + ) + + int followBehavior = GetDefaultNPCFollowBehavior( drone ) + drone.SetOwner( ent ) + drone.InitFollowBehavior( ent, followBehavior ) + drone.EnableBehavior( "Follow" ) + + for ( ;; ) + { + if ( !DroneCanRepairTarget( drone, ent, attachName ) ) + return + + DroneRepairFX( drone, ent, attachName ) + + local maxHealth = ent.GetMaxHealth() + local healAmount = maxHealth * 0.015 // 0.005 + float healTime = RandomFloatRange( 0.8, 1.2 ) + + for ( float i = 0.0; i < healTime; i++ ) + { + if ( !IsAlive( ent ) ) + return + + local newHealth = ent.GetHealth() + healAmount + newHealth = min( newHealth, maxHealth ) + ent.SetHealth( newHealth ) + WaitFrame() + } + } +} + +function DroneRepairFX( drone, ent, attachName ) +{ + // Control point sets the end position of the effect + entity cpEnd = CreateEntity( "info_placement_helper" ) + SetTargetName( cpEnd, UniqueString( "arc_cannon_beam_cpEnd" ) ) + cpEnd.SetParent( ent, attachName, false, 0.0 ) + DispatchSpawn( cpEnd ) + + entity zapBeam = CreateEntity( "info_particle_system" ) + zapBeam.kv.cpoint1 = cpEnd.GetTargetName() + + zapBeam.SetValueForEffectNameKey( ARC_CANNON_BEAM_EFFECT ) + zapBeam.kv.start_active = 0 + zapBeam.SetOwner( drone ) + zapBeam.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) + zapBeam.SetParent( drone, "ORIGIN", false, 0.0 ) + DispatchSpawn( zapBeam ) + + zapBeam.Fire( "Start" ) + zapBeam.Fire( "StopPlayEndCap", "", 2.0 ) + zapBeam.Kill_Deprecated_UseDestroyInstead( 2.0 ) + cpEnd.Kill_Deprecated_UseDestroyInstead( 2.0 ) +} + + +function SetRepairDroneTarget( entity drone, entity repairTitan ) +{ + Assert( IsAlive( repairTitan ), "Repair target " + repairTitan + " is dead" ) + Assert( repairTitan.IsTitan() ) + drone.e.repairSoul = repairTitan.GetTitanSoul() +} |