diff options
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut')
-rw-r--r-- | Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut | 736 |
1 files changed, 736 insertions, 0 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut new file mode 100644 index 000000000..68e888f41 --- /dev/null +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_ai_superspectre.nut @@ -0,0 +1,736 @@ +untyped + +global function AiSuperspectre_Init + +global function SuperSpectre_OnGroundSlamImpact +global function SuperSpectre_OnGroundLandImpact +global function SuperSpectreThink +global function SuperSpectreOnLeeched +global function SuperSpectre_WarpFall +global function CreateExplosionInflictor +global function FragDroneDeplyAnimation +global function ForceTickLaunch + +global function Reaper_LaunchFragDrone_Think +global function ReaperMinionLauncherThink + +//============================================================== +// AI Super Spectre +// +// Super Spectre keeps an array of the minions it spawned. +// Each of those minions has a reference back to it's "master." +//============================================================== +const FRAG_DRONE_BATCH_COUNT = 10 +const FRAG_DRONE_IN_FRONT_COUNT = 2 +const FRAG_DRONE_MIN_LAUNCH_COUNT = 4 +const FRAG_DRONE_LAUNCH_INTIAL_DELAY_MIN = 10 +const FRAG_DRONE_LAUNCH_INTIAL_DELAY_MAX = 20 +const FRAG_DRONE_LAUNCH_INTERVAL = 40 +const SPAWN_ENEMY_TOO_CLOSE_RANGE_SQR = 1048576 // Don't spawn guys if the target enemy is closer than this range (1024^2). +const SPAWN_HIDDEN_ENEMY_WITHIN_RANGE_SQR = 1048576 // If the enemy can't bee seen, and they are within in this range (1024^2), spawn dudes to find him. +const SPAWN_ENEMY_ABOVE_HEIGHT = 128 // If the enemy is at least this high up, then spawn dudes to find him. +const SPAWN_FUSE_TIME = 2.0 // How long after being fired before the spawner explodes and spawns a spectre. +const SPAWN_PROJECTILE_AIR_TIME = 3.0 // How long the spawn project will be in the air before hitting the ground. +const SPECTRE_EXPLOSION_DMG_MULTIPLIER = 1.2 // +20% +const DEV_DEBUG_PRINTS = false + +struct +{ + int activeMinions_GlobalArrayIdx = -1 +} file + +function AiSuperspectre_Init() +{ + PrecacheParticleSystem( $"P_sup_spectre_death" ) + PrecacheParticleSystem( $"P_sup_spectre_death_nuke" ) + PrecacheParticleSystem( $"P_xo_damage_fire_2" ) + PrecacheParticleSystem( $"P_sup_spec_dam_vent_1" ) + PrecacheParticleSystem( $"P_sup_spec_dam_vent_2" ) + PrecacheParticleSystem( $"P_sup_spectre_dam_1" ) + PrecacheParticleSystem( $"P_sup_spectre_dam_2" ) + PrecacheParticleSystem( $"drone_dam_smoke_2" ) + PrecacheParticleSystem( $"P_wpn_muzzleflash_sspectre" ) + + PrecacheImpactEffectTable( "superSpectre_groundSlam_impact" ) + PrecacheImpactEffectTable( "superSpectre_megajump_land" ) + + RegisterSignal( "SuperSpectre_OnGroundSlamImpact" ) + RegisterSignal( "SuperSpectre_OnGroundLandImpact" ) + RegisterSignal( "SuperSpectreThinkRunning" ) + RegisterSignal( "OnNukeBreakingDamage" ) // enough damage to break out or skip nuke + RegisterSignal( "death_explosion" ) + RegisterSignal( "WarpfallComplete" ) + RegisterSignal( "BeginLaunchAttack" ) + + AddDeathCallback( "npc_super_spectre", SuperSpectreDeath ) + AddDamageCallback( "npc_super_spectre", SuperSpectre_OnDamage ) + //AddPostDamageCallback( "npc_super_spectre", SuperSpectre_PostDamage ) + + file.activeMinions_GlobalArrayIdx = CreateScriptManagedEntArray() +} + +void function SuperSpectre_OnDamage( entity npc, var damageInfo ) +{ + int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo ) + if ( damageSourceId == eDamageSourceId.suicideSpectreAoE ) + { + // super spectre takes reduced damage from suicide spectres + DamageInfo_ScaleDamage( damageInfo, 0.666 ) + } +} + +void function SuperSpectre_PostDamage( entity npc, var damageInfo ) +{ + float switchRatio = 0.33 + float ratio = HealthRatio( npc ) + if ( ratio < switchRatio ) + return + float newRatio = ( npc.GetHealth() - DamageInfo_GetDamage( damageInfo ) ) / npc.GetMaxHealth() + if ( newRatio >= switchRatio ) + return + + // destroy body groups + int bodygroup + bodygroup = npc.FindBodyGroup( "lowerbody" ) + npc.SetBodygroup( bodygroup, 1 ) + bodygroup = npc.FindBodyGroup( "upperbody" ) + npc.SetBodygroup( bodygroup, 1 ) +} + +void function SuperSpectreDeath( entity npc, var damageInfo ) +{ + thread DoSuperSpectreDeath( npc, damageInfo ) +} + +void function SuperSpectreNukes( entity npc, entity attacker ) +{ + npc.EndSignal( "OnDestroy" ) + vector origin = npc.GetWorldSpaceCenter() + EmitSoundAtPosition( npc.GetTeam(), origin, "ai_reaper_nukedestruct_explo_3p" ) + PlayFX( $"P_sup_spectre_death_nuke", origin, npc.GetAngles() ) + + thread SuperSpectreNukeDamage( npc.GetTeam(), origin, attacker ) + WaitFrame() // so effect has time to grow and cover the swap to gibs + npc.Gib( <0,0,100> ) +} + +void function DoSuperSpectreDeath( entity npc, var damageInfo ) +{ + // destroyed? + if ( !IsValid( npc ) ) + return + + entity attacker = DamageInfo_GetAttacker( damageInfo ) + + const int SUPER_SPECTRE_NUKE_DEATH_THRESHOLD = 300 + + bool giveBattery = ( npc.ai.shouldDropBattery && IsSingleplayer() ) + + if ( !ShouldNukeOnDeath( npc ) || !npc.IsOnGround() || !npc.IsInterruptable() || DamageInfo_GetDamage( damageInfo ) > SUPER_SPECTRE_NUKE_DEATH_THRESHOLD || ( IsValid( attacker ) && attacker.IsTitan() ) ) + { + // just boom + vector origin = npc.GetWorldSpaceCenter() + EmitSoundAtPosition( npc.GetTeam(), origin, "ai_reaper_explo_3p" ) + npc.Gib( DamageInfo_GetDamageForce( damageInfo ) ) + if ( giveBattery ) + SpawnTitanBatteryOnDeath( npc, null ) + + return + } + + npc.ai.killShotSound = false + npc.EndSignal( "OnDestroy" ) + + entity nukeFXInfoTarget = CreateEntity( "info_target" ) + nukeFXInfoTarget.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT + DispatchSpawn( nukeFXInfoTarget ) + + nukeFXInfoTarget.SetParent( npc, "HIJACK" ) + + EmitSoundOnEntity( nukeFXInfoTarget, "ai_reaper_nukedestruct_warmup_3p" ) + + AI_CreateDangerousArea_DamageDef( damagedef_reaper_nuke, nukeFXInfoTarget, TEAM_INVALID, true, true ) + + OnThreadEnd( + function() : ( nukeFXInfoTarget, npc, attacker, giveBattery ) + { + if ( IsValid( nukeFXInfoTarget ) ) + { + StopSoundOnEntity( nukeFXInfoTarget, "ai_reaper_nukedestruct_warmup_3p" ) + nukeFXInfoTarget.Destroy() + } + + + if ( IsValid( npc ) ) + { + thread SuperSpectreNukes( npc, attacker ) + if ( giveBattery ) + { + SpawnTitanBatteryOnDeath( npc, null ) + } + } + } + ) + + //int bodygroup = npc.FindBodyGroup( "upperbody" ) + //npc.SetBodygroup( bodygroup, 1 ) + + // TODO: Add death sound + + WaitSignalOnDeadEnt( npc, "death_explosion" ) +} + +entity function CreateExplosionInflictor( vector origin ) +{ + entity inflictor = CreateEntity( "script_ref" ) + inflictor.SetOrigin( origin ) + inflictor.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT + DispatchSpawn( inflictor ) + return inflictor +} + +void function SuperSpectreNukeDamage( int team, vector origin, entity attacker ) +{ + // all damage must have an inflictor currently + entity inflictor = CreateExplosionInflictor( origin ) + + OnThreadEnd( + function() : ( inflictor ) + { + if ( IsValid( inflictor ) ) + inflictor.Destroy() + } + ) + + int explosions = 8 + float time = 1.0 + + for ( int i = 0; i < explosions; i++ ) + { + entity explosionOwner + if ( IsValid( attacker ) ) + explosionOwner = attacker + else + explosionOwner = GetTeamEnt( team ) + + RadiusDamage_DamageDefSimple( + damagedef_reaper_nuke, + origin, // origin + explosionOwner, // owner + inflictor, // inflictor + 0 ) // dist from attacker + + wait RandomFloatRange( 0.01, 0.21 ) + } +} + +void function SuperSpectre_OnGroundLandImpact( entity npc ) +{ + PlayImpactFXTable( npc.GetOrigin(), npc, "superSpectre_megajump_land", SF_ENVEXPLOSION_INCLUDE_ENTITIES ) +} + + +void function SuperSpectre_OnGroundSlamImpact( entity npc ) +{ + PlayGroundSlamFX( npc ) +} + + +function PlayGroundSlamFX( entity npc ) +{ + int attachment = npc.LookupAttachment( "muzzle_flash" ) + vector origin = npc.GetAttachmentOrigin( attachment ) + PlayImpactFXTable( origin, npc, "superSpectre_groundSlam_impact", SF_ENVEXPLOSION_INCLUDE_ENTITIES ) +} + + +bool function EnemyWithinRangeSqr( entity npc, entity enemy, float range ) +{ + vector pos = npc.GetOrigin() + vector enemyPos = enemy.GetOrigin() + float distance = DistanceSqr( pos, enemyPos ) + + return distance <= range +} + +bool function ShouldLaunchFragDrones( entity npc, int activeMinions_EntArrayID ) +{ +// printt( "active " + GetScriptManagedEntArrayLen( activeMinions_EntArrayID ) ) + if ( !npc.ai.superSpectreEnableFragDrones ) + return false + + // check global minions + if ( GetScriptManagedEntArrayLen( file.activeMinions_GlobalArrayIdx ) > 5 ) + return false + + // only launch if all minions are dead + if ( GetScriptManagedEntArrayLen( activeMinions_EntArrayID ) > 5 ) + return false + + entity enemy = npc.GetEnemy() + + // Only spawn dudes if we have an enemy + if ( !IsValid( enemy ) ) + return false + + vector ornull lkp = npc.LastKnownPosition( enemy ) + if ( lkp == null ) + return false + + expect vector( lkp ) + + // Don't spawn if the enemy is too far away + if ( Distance( npc.GetOrigin(), lkp ) > 1500 ) + return false + + return true +} + +function SuperSpectreOnLeeched( npc, player ) +{ + local maxHealth = npc.GetMaxHealth() + npc.SetHealth( maxHealth * 0.5 ) // refill to half health +} + +function SuperSpectreThink( entity npc ) +{ + npc.EndSignal( "OnDeath" ) + + int team = npc.GetTeam() + + int activeMinions_EntArrayID = CreateScriptManagedEntArray() + if ( npc.kv.squadname == "" ) + SetSquad( npc, UniqueString( "super_spec_squad" ) ) + + npc.ai.superSpectreEnableFragDrones = expect int( npc.Dev_GetAISettingByKeyField( "enable_frag_drones" ) ) == 1 + + OnThreadEnd ( + function() : ( activeMinions_EntArrayID, npc, team ) + { + entity owner + if ( IsValid( npc ) ) + owner = npc + + foreach ( minion in GetScriptManagedEntArray( activeMinions_EntArrayID ) ) + { + // Self destruct the suicide spectres if applicable + if ( minion.GetClassName() != "npc_frag_drone" ) + continue + + if ( minion.ai.suicideSpectreExplodingAttacker == null ) + minion.TakeDamage( minion.GetHealth(), owner, owner, { scriptType = DF_DOOMED_HEALTH_LOSS, damageSourceId = eDamageSourceId.mp_weapon_super_spectre } ) + } + } + ) + + wait RandomFloatRange( FRAG_DRONE_LAUNCH_INTIAL_DELAY_MIN, FRAG_DRONE_LAUNCH_INTIAL_DELAY_MAX ) + + npc.kv.doScheduleChangeSignal = true + + while ( 1 ) + { + if ( ShouldLaunchFragDrones( npc, activeMinions_EntArrayID ) ) + waitthread SuperSpectre_LaunchFragDrone_Think( npc, activeMinions_EntArrayID ) + + wait FRAG_DRONE_LAUNCH_INTERVAL + } +} + +void function SuperSpectre_LaunchFragDrone_Think( entity npc, int activeMinions_EntArrayID ) +{ + array<vector> targetOrigins = GetFragDroneTargetOrigins( npc, npc.GetOrigin(), 200, 2000, 64, FRAG_DRONE_BATCH_COUNT ) + + if ( targetOrigins.len() < FRAG_DRONE_MIN_LAUNCH_COUNT ) + return + + npc.RequestSpecialRangeAttack( targetOrigins.len() + FRAG_DRONE_IN_FRONT_COUNT ) + + // wait for first attack signal + npc.WaitSignal( "OnSpecialAttack" ) + npc.EndSignal( "OnDeath" ) + npc.EndSignal( "OnScheduleChange" ) // kv.doScheduleChangeSignal = true + + // drop a few in front of enemy view + entity enemy = npc.GetEnemy() + if ( enemy ) + { + vector searchOrigin = enemy.GetOrigin() + ( enemy.GetForwardVector() * 400 ) + array<vector> frontOfEnemyOrigins = GetFragDroneTargetOrigins( npc, searchOrigin, 0, 500, 16, FRAG_DRONE_IN_FRONT_COUNT ) + + foreach ( targetOrigin in frontOfEnemyOrigins ) + { + thread LaunchSpawnerProjectile( npc, targetOrigin, activeMinions_EntArrayID ) + //DebugDrawBox( targetOrigin, Vector(-10, -10, 0), Vector(10, 10, 10), 255, 0, 0, 255, 5 ) + npc.WaitSignal( "OnSpecialAttack" ) + } + } + + // drop rest in pre-searched spots + foreach ( targetOrigin in targetOrigins ) + { + thread LaunchSpawnerProjectile( npc, targetOrigin, activeMinions_EntArrayID ) + npc.WaitSignal( "OnSpecialAttack" ) + } +} + +void function ReaperMinionLauncherThink( entity reaper ) +{ + if ( GetBugReproNum() != 221936 ) + reaper.kv.squadname = "" + + StationaryAIPosition launchPos = GetClosestAvailableStationaryPosition( reaper.GetOrigin(), 8000, eStationaryAIPositionTypes.LAUNCHER_REAPER ) + launchPos.inUse = true + + OnThreadEnd( + function () : ( launchPos ) + { + launchPos.inUse = false + } + ) + + reaper.EndSignal( "OnDeath" ) + reaper.AssaultSetFightRadius( 96 ) + reaper.AssaultSetGoalRadius( reaper.GetMinGoalRadius() ) + + while ( true ) + { + WaitFrame() + + if ( Distance( reaper.GetOrigin(), launchPos.origin ) > 96 ) + { + printt( reaper," ASSAULT:", launchPos.origin, Distance( reaper.GetOrigin(), launchPos.origin ) ) + reaper.AssaultPoint( launchPos.origin ) + table signalData = WaitSignal( reaper, "OnFinishedAssault", "OnEnterGoalRadius", "OnFailedToPath" ) + printt( reaper," END ASSAULT:", launchPos.origin, signalData.signal ) + if ( signalData.signal == "OnFailedToPath" ) + continue + } + + printt( reaper," LAUNCH:", launchPos.origin ) + waitthread Reaper_LaunchFragDrone_Think( reaper, "npc_frag_drone_fd" ) + printt( reaper," END LAUNCH:", launchPos.origin ) + while ( GetScriptManagedEntArrayLen( reaper.ai.activeMinionEntArrayID ) > 2 ) + WaitFrame() + } +} + +void function Reaper_LaunchFragDrone_Think( entity reaper, string fragDroneSettings = "" ) +{ + if ( reaper.ai.activeMinionEntArrayID < 0 ) + reaper.ai.activeMinionEntArrayID = CreateScriptManagedEntArray() + + int activeMinions_EntArrayID = reaper.ai.activeMinionEntArrayID + + const int MAX_TICKS = 4 + + int currentMinions = GetScriptManagedEntArray( reaper.ai.activeMinionEntArrayID ).len() + int minionsToSpawn = MAX_TICKS - currentMinions + + if ( minionsToSpawn <= 0 ) + return + + array<vector> targetOrigins = GetFragDroneTargetOrigins( reaper, reaper.GetOrigin(), 200, 2000, 64, MAX_TICKS ) + + if ( targetOrigins.len() < minionsToSpawn ) + return + + if ( IsAlive( reaper.GetEnemy() ) && ( reaper.GetEnemy().IsPlayer() || reaper.GetEnemy().IsNPC() ) && reaper.CanSee( reaper.GetEnemy() ) ) + return + + OnThreadEnd( + function() : ( reaper ) + { + if ( IsValid( reaper ) ) + { + reaper.Anim_Stop() + } + } + ) + + printt( reaper, " BEGIN LAUNCHING: ", minionsToSpawn, reaper.GetCurScheduleName() ) + + reaper.EndSignal( "OnDeath" ) + + while ( !reaper.IsInterruptable() ) + WaitFrame() + + waitthread PlayAnim( reaper, "sspec_idle_to_speclaunch" ) + + while ( minionsToSpawn > 0 ) + { + // drop rest in pre-searched spots + foreach ( targetOrigin in targetOrigins ) + { + if ( minionsToSpawn <= 0 ) + break + + printt( reaper, " LAUNCHING: ", minionsToSpawn ) + thread LaunchSpawnerProjectile( reaper, targetOrigin, activeMinions_EntArrayID, fragDroneSettings ) + minionsToSpawn-- + + if ( minionsToSpawn <= 0 ) + break + + waitthread PlayAnim( reaper, "sspec_speclaunch_fire" ) + } + } + + waitthread PlayAnim( reaper, "sspec_speclaunch_to_idle" ) +} + + + +array<vector> function GetFragDroneTargetOrigins( entity npc, vector origin, float minRadius, float maxRadius, int randomCount, int desiredCount ) +{ + array<vector> targetOrigins +/* + vector angles = npc.GetAngles() + angles.x = 0 + angles.z = 0 + + vector origin = npc.GetOrigin() + Vector( 0, 0, 1 ) + float arc = 0 + float dist = 200 + + for ( ;; ) + { + if ( dist > 2000 || targetOrigins.len() >= 12 ) + break + + angles = AnglesCompose( angles, <0,arc,0> ) + arc += 35 + arc %= 360 + dist += 200 + + vector ornull tryOrigin = TryCreateFragDroneLaunchTrajectory( npc, origin, angles, dist ) + if ( tryOrigin == null ) + continue + expect vector( tryOrigin ) + targetOrigins.append( tryOrigin ) + } +*/ + float traceFrac = TraceLineSimple( origin, origin + <0, 0, 200>, npc ) + if ( traceFrac < 1 ) + return targetOrigins; + + array< vector > randomSpots = NavMesh_RandomPositions_LargeArea( origin, HULL_HUMAN, randomCount, minRadius, maxRadius ) + + int numFragDrones = 0 + foreach( spot in randomSpots ) + { + targetOrigins.append( spot ) + numFragDrones++ + if ( numFragDrones == desiredCount ) + break + } + + return targetOrigins +} + +vector ornull function TryCreateFragDroneLaunchTrajectory( entity npc, vector origin, vector angles, float dist ) +{ + vector forward = AnglesToForward( angles ) + vector targetOrigin = origin + forward * dist + + vector ornull clampedPos = NavMesh_ClampPointForHullWithExtents( targetOrigin, HULL_HUMAN, < 300, 300, 100 > ) + + if ( clampedPos == null ) + return null + + vector vel = GetVelocityForDestOverTime( origin, expect vector( clampedPos ), SPAWN_PROJECTILE_AIR_TIME ) + float traceFrac = TraceLineSimple( origin, origin + vel, npc ) + //DebugDrawLine( origin, origin + vel, 255, 0, 0, true, 5.0 ) + if ( traceFrac >= 0.5 ) + return clampedPos + return null +} + +void function FragDroneDeplyAnimation( entity drone, float minDelay = 0.5, float maxDelay = 2.5 ) +{ + Assert( !drone.ai.fragDroneArmed, "Armed drone was told to play can animation. Spawn drone with CreateFragDroneCan()" ) + drone.EndSignal( "OnDeath" ) + + drone.SetInvulnerable() + OnThreadEnd( + function() : ( drone ) + { + drone.ClearInvulnerable() + } + ) + + drone.Anim_ScriptedPlay( "sd_closed_idle" ) + wait RandomFloatRange( minDelay, maxDelay ) + + #if MP + while ( !drone.IsInterruptable() ) + { + WaitFrame() + } + #endif + + drone.Anim_ScriptedPlay( "sd_closed_to_open" ) + + // Wait for P_drone_frag_open_flicker FX to play inside sd_closed_to_open + wait 0.6 +} + +void function LaunchSpawnerProjectile( entity npc, vector targetOrigin, int activeMinions_EntArrayID, string droneSettings = "" ) +{ + //npc.EndSignal( "OnDeath" ) + + entity weapon = npc.GetOffhandWeapon( 0 ) + + if ( !IsValid( weapon ) ) + return + + int id = npc.LookupAttachment( "launch" ) + vector launchPos = npc.GetAttachmentOrigin( id ) + int team = npc.GetTeam() + vector launchAngles = npc.GetAngles() + string squadname = expect string( npc.kv.squadname ) + vector vel = GetVelocityForDestOverTime( launchPos, targetOrigin, SPAWN_PROJECTILE_AIR_TIME ) + +// DebugDrawLine( npc.GetOrigin() + <3,3,3>, launchPos + <3,3,3>, 255, 0, 0, true, 5.0 ) + float armTime = SPAWN_PROJECTILE_AIR_TIME + RandomFloatRange( 1.0, 2.5 ) + entity nade = weapon.FireWeaponGrenade( launchPos, vel, <200,0,0>, armTime, damageTypes.dissolve, damageTypes.explosive, PROJECTILE_NOT_PREDICTED, true, true ) + + AddToScriptManagedEntArray( activeMinions_EntArrayID, nade ) + AddToScriptManagedEntArray( file.activeMinions_GlobalArrayIdx, nade ) + + nade.SetOwner( npc ) + nade.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( nade, team, activeMinions_EntArrayID, squadname, droneSettings ) + { + vector origin = nade.GetOrigin() + vector angles = nade.GetAngles() + + vector ornull clampedPos = NavMesh_ClampPointForHullWithExtents( origin, HULL_HUMAN, < 100, 100, 100 > ) + if ( clampedPos == null ) + return + + entity drone = CreateFragDroneCan( team, expect vector( clampedPos ), < 0, angles.y, 0 > ) + SetSpawnOption_SquadName( drone, squadname ) + if ( droneSettings != "" ) + { + SetSpawnOption_AISettings( drone, droneSettings ) + } + drone.kv.spawnflags = SF_NPC_ALLOW_SPAWN_SOLID // clamped to navmesh no need to check solid + DispatchSpawn( drone ) + + thread FragDroneDeplyAnimation( drone ) + + AddToScriptManagedEntArray( activeMinions_EntArrayID, drone ) + AddToScriptManagedEntArray( file.activeMinions_GlobalArrayIdx, drone ) + } + ) + + Grenade_Init( nade, weapon ) + + EmitSoundOnEntity( npc, "SpectreLauncher_AI_WpnFire" ) + WaitForever() + +// wait SPAWN_PROJECTILE_AIR_TIME + SPAWN_FUSE_TIME +} + + +// Seriously don't use this unless absolutely necessary! Used for scripted moment in Reapertown. +// Bypasses all of the tick launch rules and sends a request for launching ticks to code immediately. +void function ForceTickLaunch( entity npc ) +{ + SuperSpectre_LaunchFragDrone_Think( npc, file.activeMinions_GlobalArrayIdx ) +} + + +/************************************************************************************************\ +######## ######## ####### ######## ####### ######## ## ## ######## ######## +## ## ## ## ## ## ## ## ## ## ## ## ## ## ## +## ## ## ## ## ## ## ## ## ## #### ## ## ## +######## ######## ## ## ## ## ## ## ## ######## ###### +## ## ## ## ## ## ## ## ## ## ## ## +## ## ## ## ## ## ## ## ## ## ## ## +## ## ## ####### ## ####### ## ## ## ######## +\************************************************************************************************/ + + +function SuperSpectre_WarpFall( entity ai ) +{ + ai.EndSignal( "OnDestroy" ) + + vector origin = ai.GetOrigin() + entity mover = CreateOwnedScriptMover( ai ) + ai.SetParent( mover, "", false, 0 ) + ai.Hide() + ai.SetEfficientMode( true ) + ai.SetInvulnerable() + + WaitFrame() // give AI time to hide before moving + + vector warpPos = origin + < 0, 0, 1000 > + mover.SetOrigin( warpPos ) + + #if GRUNTCHATTER_ENABLED + GruntChatter_TryIncomingSpawn( ai, origin ) + #endif + + EmitSoundAtPosition( TEAM_UNASSIGNED, origin, "Titan_1P_Warpfall_Start" ) + + local e = {} + e.warpfx <- PlayFX( TURBO_WARP_FX, warpPos + < 0, 0, -104 >, mover.GetAngles() ) + e.smokeFx <- null + + OnThreadEnd( + function() : ( e, mover, ai ) + { + if ( IsAlive( ai ) ) + { + ai.ClearParent() + ai.SetVelocity( <0,0,0> ) + ai.Signal( "WarpfallComplete" ) + } + if ( IsValid( e.warpfx ) ) + e.warpfx.Destroy() + if ( IsValid( e.smokeFx ) ) + e.smokeFx.Destroy() + if ( IsValid( mover ) ) + mover.Destroy() + } + ) + wait 0.5 + + EmitSoundAtPosition( TEAM_UNASSIGNED, origin, "Titan_3P_Warpfall_WarpToLanding" ) + + wait 0.4 + + ai.Show() + + e.smokeFx = PlayFXOnEntity( TURBO_WARP_COMPANY, ai, "", <0.0, 0.0, 152.0> ) + + local time = 0.2 + mover.MoveTo( origin, time, 0, 0 ) + wait time + + ai.SetEfficientMode( false ) + ai.ClearInvulnerable() + + e.smokeFx.Destroy() + PlayFX( $"droppod_impact", origin ) + + Explosion_DamageDefSimple( + damagedef_reaper_fall, + origin, + ai, // attacker + ai, // inflictor + origin ) + + wait 0.1 +} + +bool function ShouldNukeOnDeath( entity ent ) +{ + if ( IsMultiplayer() ) + return false + + return ent.Dev_GetAISettingByKeyField( "nuke_on_death" ) == 1 +}
\ No newline at end of file |