aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts/ai
diff options
context:
space:
mode:
authorBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-08-31 23:14:58 +0100
committerBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-08-31 23:14:58 +0100
commit9a96d0bff56f1969c68bb52a2f33296095bdc67d (patch)
tree4175928e488632705692e3cccafa1a38dd854615 /Northstar.CustomServers/mod/scripts/vscripts/ai
parent27bd240871b7c0f2f49fef137718b2e3c208e3b4 (diff)
downloadNorthstarMods-9a96d0bff56f1969c68bb52a2f33296095bdc67d.tar.gz
NorthstarMods-9a96d0bff56f1969c68bb52a2f33296095bdc67d.zip
move to new mod format
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/ai')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_boss_titan.gnut794
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_chatter.gnut129
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_cloak_drone.gnut678
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_drone.gnut1388
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_emp_titans.gnut181
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_gunship.gnut97
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_lethality.gnut97
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_faces.gnut226
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_jobs.gnut600
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvins.gnut141
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_spectres.gnut7
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut395
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_nuke_titans.gnut129
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_personal_shield.gnut371
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_pilots.gnut808
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_sniper_titans.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut787
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_mp.gnut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_sp.gnut17
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn.gnut696
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn_content.gnut879
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spectre.gnut131
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut606
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stationary_firing_positions.gnut261
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_suicide_spectres.gnut576
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret.gnut24
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret_sentry.gnut72
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_utility.gnut558
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod.gnut187
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod_fireteam.gnut246
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_grunt_chatter.gnut1786
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_squad_spawn.gnut167
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_titan_npc_behavior.gnut404
33 files changed, 13440 insertions, 0 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_boss_titan.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_boss_titan.gnut
new file mode 100644
index 000000000..da3058d78
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_boss_titan.gnut
@@ -0,0 +1,794 @@
+global function PlayerParentTest
+
+global function AIBossTitan_Init
+global function OnBossTitanPrimaryFire
+global function IsVDUTitan
+global function IsBossTitan
+global function GetBossTitanCharacterModel
+
+global function BossTitanRetreat
+global function BossTitanAdvance
+global function IsMercTitan
+global function GetMercCharacterID
+global function BossTitanIntro
+global function BossTitanVDUEnabled
+global function BossTitanPlayerView
+
+global function MakeMidHealthTitan
+
+global const float SLAMZOOM_TIME = 1.0
+global const float BOSS_TITAN_CORE_DAMAGE_SCALER_LOW = 0.6
+global const float BOSS_TITAN_CORE_DAMAGE_SCALER = 0.5
+
+void function AIBossTitan_Init()
+{
+ if ( IsMultiplayer() )
+ return
+
+ FlagInit( "BossTitanViewFollow" )
+
+ AddSpawnCallback( "npc_titan", NPCTitanSpawned )
+ AddDeathCallback( "npc_titan", OnBossTitanDeath )
+ AddCallback_OnTitanDoomed( OnBossTitanDoomed )
+ AddCallback_OnTitanHealthSegmentLost( OnTitanLostSegment )
+
+ AddSyncedMeleeServerCallback( GetSyncedMeleeChooser( "titan", "titan" ), OnBossTitanExecuted )
+
+ PrecacheParticleSystem( $"P_VDU_mflash" )
+
+ RegisterSignal( "BossTitanStartAnim" )
+ RegisterSignal( "BossTitanIntroEnded" )
+}
+
+void function OnBossTitanExecuted( SyncedMeleeChooser actions, SyncedMelee action, entity attacker, entity victim )
+{
+ if ( victim.IsNPC() && IsVDUTitan( victim ) && BossTitanVDUEnabled( victim ) )
+ {
+ string name = victim.ai.bossCharacterName == "" ? "Generic1" : victim.ai.bossCharacterName
+ int bossID = GetBossTitanID( name )
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player == attacker || IsMercTitan( victim ) )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanDeath", victim.GetEncodedEHandle(), bossID )
+ }
+ }
+ }
+}
+
+void function OnBossTitanDeath( entity titan, var damageInfo )
+{
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( damageSourceId == eDamageSourceId.titan_execution )
+ return
+
+ entity soul = titan.GetTitanSoul()
+ if ( soul.IsEjecting() )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( IsVDUTitan( titan ) && BossTitanVDUEnabled( titan ) )
+ {
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player == attacker || IsMercTitan( titan ) )
+ {
+ string name = titan.ai.bossCharacterName == "" ? "Generic1" : titan.ai.bossCharacterName
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanDeath", titan.GetEncodedEHandle(), GetBossTitanID( name ) )
+ }
+ }
+ }
+}
+
+void function OnBossTitanDoomed( entity titan, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( IsVDUTitan( titan ) && BossTitanVDUEnabled( titan ) )
+ {
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player == attacker || IsMercTitan( titan ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanDoomed", titan.GetEncodedEHandle() )
+ }
+ }
+}
+
+void function OnBossTitanCoreMitigation( entity titan, var damageInfo )
+{
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ switch ( damageSourceID )
+ {
+ case eDamageSourceId.mp_titancore_salvo_core:
+ DamageInfo_ScaleDamage( damageInfo, BOSS_TITAN_CORE_DAMAGE_SCALER_LOW )
+ return
+
+ // case eDamageSourceId.mp_titancore_laser_cannon: laser core handles this in mp_titanweapon_lasercannon.nut
+ case eDamageSourceId.mp_titancore_flame_wave:
+ case eDamageSourceId.mp_titancore_flame_wave_secondary:
+ case eDamageSourceId.mp_titancore_shift_core:
+ case eDamageSourceId.mp_titanweapon_flightcore_rockets:
+ case eDamageSourceId.mp_titancore_amp_core:
+ case damagedef_nuclear_core:
+ DamageInfo_ScaleDamage( damageInfo, BOSS_TITAN_CORE_DAMAGE_SCALER )
+ return
+ }
+
+ // SMART CORE
+ array<string> weaponMods = GetWeaponModsFromDamageInfo( damageInfo )
+ if ( weaponMods.contains( "Smart_Core" ) )
+ {
+ DamageInfo_ScaleDamage( damageInfo, BOSS_TITAN_CORE_DAMAGE_SCALER )
+ // DamageInfo_ScaleDamage( damageInfo, BOSS_TITAN_CORE_DAMAGE_SCALER_LOW )
+ return
+ }
+}
+
+void function NPCTitanSpawned( entity titan )
+{
+ Assert( !IsMultiplayer() )
+
+ if ( titan.GetTeam() == TEAM_IMC )
+ {
+ switch ( titan.ai.bossTitanType )
+ {
+ case TITAN_WEAK:
+ case TITAN_HENCH:
+ MakeMidHealthTitan( titan )
+
+ case TITAN_BOSS:
+ RegisterBossTitan( titan )
+ ApplyTitanDamageState( titan )
+
+ if ( titan.ai.bossTitanType == TITAN_BOSS )
+ AddEntityCallback_OnDamaged( titan, OnBossTitanCoreMitigation )
+
+ if ( titan.HasKey( "skip_boss_intro" ) && titan.GetValueForKey( "skip_boss_intro" ) == "1" )
+ return
+ thread BossTitanNoIntro( titan )
+ break;
+
+
+ case TITAN_MERC:
+ // TODO: This SetSkin() call should move to RegisterBossTitan() when the above TITAN_BOSS stuff is cleaned up/removed.
+ titan.SetSkin( 1 ) // all titan models have a boss titan version of the skin at index 1
+ RegisterBossTitan( titan )
+ ApplyTitanDamageState( titan )
+
+ AddEntityCallback_OnDamaged( titan, OnBossTitanCoreMitigation )
+
+ if ( titan.HasKey( "skip_boss_intro" ) && titan.GetValueForKey( "skip_boss_intro" ) == "1" )
+ return
+
+ if ( !titan.ai.bossTitanPlayIntro )
+ return
+
+ foreach ( player in GetPlayerArray() )
+ {
+ thread BossTitanIntro( player, titan )
+ }
+ break
+
+ // case TITAN_WEAK:
+ // MakeLowHealthTitan( titan )
+ // break
+
+ case TITAN_AUTO:
+ if ( !IsMultiplayer() && GetMapName() == "sp_hub_timeshift" || GetMapName() == "sp_timeshift_spoke02" )
+ MakeLowHealthTitan( titan )
+ break
+ default:
+ return
+ }
+ }
+}
+
+void function BossTitanNoIntro( entity titan )
+{
+ FlagWait( "PlayerDidSpawn" )
+
+ entity player = GetPlayerArray()[0]
+
+ player.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDeath" )
+
+ // Wait until player sees the boss titan
+ waitthread WaitForHotdropToEnd( titan )
+
+ while ( 1 )
+ {
+ waitthread WaitTillLookingAt( player, titan, true, 60, 5100 )
+ if ( titan.GetEnemy() == null )
+ titan.WaitSignal( "OnSeeEnemy" )
+ else
+ break
+ }
+
+ if ( BossTitanVDUEnabled( titan ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanNoIntro", titan.GetEncodedEHandle() )
+ AddEntityCallback_OnDamaged( titan, OnBossTitanDamaged )
+ AddTitanCallback_OnHealthSegmentLost( titan, OnBossTitanLostSegment )
+}
+
+void function BossTitanIntro( entity player, entity titan, BossTitanIntroData ornull introdata = null )
+{
+ Assert( titan.IsNPC() )
+ Assert( titan.ai.bossCharacterName != "" )
+
+ if ( introdata == null )
+ {
+ BossTitanIntroData defaultData = GetBossTitanIntroData( titan.ai.bossCharacterName )
+ introdata = defaultData
+ }
+
+ expect BossTitanIntroData( introdata )
+
+ player.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDeath" )
+
+ HideCrit( titan )
+ titan.SetValidHealthBarTarget( false )
+ titan.SetInvulnerable()
+
+ // Wait until player sees the boss titan
+
+ while ( titan.e.isHotDropping )
+ {
+ WaitFrame()
+ }
+
+ HideName( titan )
+ titan.kv.allowshoot = 0
+
+ if ( introdata.waitToStartFlag != "" )
+ FlagWait( introdata.waitToStartFlag )
+
+ if ( introdata.waitForLookat )
+ waitthread WaitTillLookingAt( player, titan, introdata.lookatDoTrace, introdata.lookatDegrees, introdata.lookatMinDist )
+
+ while ( IsPlayerDisembarking( player ) || IsPlayerEmbarking( player ) )
+ {
+ WaitFrame()
+ }
+
+ BossTitanData bossTitanData = GetBossTitanData( titan.ai.bossCharacterName )
+
+ // Create a ref node to animate on
+ vector refPos
+ vector refAngles
+
+ if ( bossTitanData.introAnimTitanRef != "" )
+ {
+ entity titanAnimRef = GetEntByScriptName( bossTitanData.introAnimTitanRef )
+ refPos = titanAnimRef.GetOrigin()
+ refAngles = titanAnimRef.GetAngles()
+ }
+ else
+ {
+ refPos = titan.GetOrigin()
+
+ vector vecToPlayer = Normalize( player.GetOrigin() - titan.GetOrigin() )
+ refAngles = VectorToAngles( vecToPlayer )
+ refAngles = FlattenAngles( refAngles )
+ }
+
+ entity ref
+ if ( introdata.parentRef != null )
+ {
+ ref = introdata.parentRef
+ }
+ else
+ ref = CreateScriptRef( refPos, refAngles )
+
+ entity soul = titan.GetTitanSoul()
+ if ( IsValid( soul.soul.bubbleShield ) )
+ {
+ soul.soul.bubbleShield.Destroy()
+ }
+
+ // Freeze player and clear up the screen
+ StartBossIntro( player, titan, introdata )
+ player.Hide()
+ player.SetVelocity( <0,0,0> )
+ player.FreezeControlsOnServer()
+ player.SetNoTarget( true )
+ player.SetInvulnerable()
+
+ // Do special player view movement
+ FlagSet( "BossTitanViewFollow" )
+
+ // Animate the boss titan
+ entity pilot = CreatePropDynamic( GetBossTitanCharacterModel( titan ) )
+ if ( introdata.parentRef != null )
+ {
+ if ( introdata.parentAttach != "" )
+ {
+ pilot.SetParent( introdata.parentRef, introdata.parentAttach )
+ }
+ else
+ {
+ pilot.SetParent( introdata.parentRef )
+ }
+ }
+ SetTeam( pilot, TEAM_IMC )
+
+ string pilotAnimName = bossTitanData.introAnimPilot
+ string titanAnimName = bossTitanData.introAnimTitan
+
+ float introDuration = 6.0
+
+ Assert( titan.Anim_HasSequence( titanAnimName ), "Your boss titan does not have an intro animation set, or it is missing." )
+
+ introDuration = titan.GetSequenceDuration( titanAnimName )
+
+ svGlobal.levelEnt.Signal( "BossTitanStartAnim" )
+
+ if ( introdata.parentAttach != "" )
+ {
+ thread PlayAnim( pilot, pilotAnimName, ref, introdata.parentAttach, 0.0 )
+ thread PlayAnim( titan, titanAnimName, ref, introdata.parentAttach, 0.0 )
+ }
+ else
+ {
+ thread PlayAnim( pilot, pilotAnimName, ref, 0.0 )
+ thread PlayAnim( titan, titanAnimName, ref, 0.0 )
+ }
+
+ Objective_Hide( player )
+
+ thread BossTitanPlayerView( player, titan, ref, bossTitanData.titanCameraAttachment )
+
+ wait introDuration - SLAMZOOM_TIME
+
+ // Player view returns to normal
+ FlagClear( "BossTitanViewFollow" )
+ EndBossIntro( player, titan )
+
+ wait SLAMZOOM_TIME
+
+ // Return the player screen and movement back to normal
+ player.UnfreezeControlsOnServer()
+ player.SetNoTarget( false )
+ player.ClearInvulnerable()
+ player.Show()
+ pilot.Destroy()
+
+ if ( IsValid( titan ) )
+ {
+ titan.ClearInvulnerable()
+ titan.Solid()
+ AddEntityCallback_OnDamaged( titan, OnBossTitanDamaged )
+ AddTitanCallback_OnHealthSegmentLost( titan, OnBossTitanLostSegment )
+ ShowName( titan )
+ titan.SetValidHealthBarTarget( true )
+ ShowCrit( titan )
+ Signal( titan, "BossTitanIntroEnded" )
+ }
+
+ wait 0.5
+
+ if ( Flag( "AutomaticCheckpointsEnabled" ) )
+ {
+ if ( introdata.checkpointOnlyIfPlayerTitan )
+ {
+ if ( player.IsTitan() )
+ CheckPoint_Forced()
+ }
+ else
+ CheckPoint_Forced()
+ }
+
+ wait 1.0
+
+ titan.kv.allowshoot = 1
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanPostIntro", titan.GetEncodedEHandle(), BossTitanVDUEnabled( titan ) )
+}
+
+void function PlayerParentTest()
+{
+ entity player = GetPlayerArray()[0]
+
+ vector moverStartPos = player.EyePosition()
+ vector moverStartAng = FlattenAngles( player.GetAngles() )
+ entity mover = CreateScriptMover( moverStartPos, moverStartAng )
+
+ player.SnapEyeAngles( moverStartAng )
+ player.SetParent( mover, "", true )
+}
+
+void function BossTitanPlayerView( entity player, entity titan, entity ref, string titanCameraAttachment )
+{
+ bool hasTitanCameraAttachment = titanCameraAttachment != ""
+
+ EndSignal( player, "OnDeath" )
+ EndSignal( titan, "OnDeath" )
+
+ vector moverStartPos = player.CameraPosition()
+
+ vector camFeetDiff = < 0,0,-185 >//player.GetOrigin() - player.CameraPosition()
+
+ vector moverStartAng = player.CameraAngles()
+ entity mover = CreateScriptMover( moverStartPos, moverStartAng )
+
+ // player.SnapEyeAngles( moverStartAng )
+ // player.SetParent( mover, "", true )
+ // ViewConeZero( player )
+
+ entity camera = CreateEntity( "point_viewcontrol" )
+ camera.kv.spawnflags = 56 // infinite hold time, snap to goal angles, make player non-solid
+
+ camera.SetOrigin( player.CameraPosition() )
+ camera.SetAngles( player.CameraAngles() )
+ DispatchSpawn( camera )
+
+ camera.SetParent( mover, "", false )
+
+ OnThreadEnd(
+ function() : ( player, titan, mover, camera )
+ {
+ if ( IsValid( camera ) )
+ {
+ camera.Destroy()
+ }
+
+ mover.Destroy()
+
+ if ( IsValid( player ) )
+ {
+ player.ClearParent()
+ player.ClearViewEntity()
+ RemoveCinematicFlag( player, CE_FLAG_HIDE_MAIN_HUD )
+ RemoveCinematicFlag( player, CE_FLAG_TITAN_3P_CAM )
+ }
+
+ if ( IsAlive( titan ) && titan.IsNPC() )
+ {
+ titan.SetNoTarget( false )
+ titan.DisableNPCFlag( NPC_IGNORE_ALL )
+ }
+ }
+ )
+
+ // Slam Zoom In
+ float slamZoomTime = SLAMZOOM_TIME
+ float slamZoomTimeAccel = 0.3
+ float slamZoomTimeDecel = 0.3
+ vector viewOffset = < 200, 100, 160 >
+
+ vector viewPos = ref.GetOrigin() + ( AnglesToForward( ref.GetAngles() ) * viewOffset.x ) + ( AnglesToRight( ref.GetAngles() ) * viewOffset.y ) + ( AnglesToUp( ref.GetAngles() ) * viewOffset.z )
+ vector viewAngles = ref.GetAngles() + <0,180,0>
+ if ( hasTitanCameraAttachment )
+ {
+ WaitFrame()
+ int titanCameraAttachmentID = titan.LookupAttachment( titanCameraAttachment )
+ viewPos = titan.GetAttachmentOrigin( titanCameraAttachmentID )
+ viewAngles = titan.GetAttachmentAngles( titanCameraAttachmentID )
+ }
+
+ float blendTime = 0.5
+ float waittime = 0.3
+ float moveTime = slamZoomTime - blendTime - waittime
+
+ float startTime = Time()
+
+ player.SetVelocity( < 0,0,0 > )
+ player.MakeInvisible()
+ HolsterAndDisableWeapons( player )
+
+ wait waittime // wait for the AI to blend into the anim
+
+ if ( titan.IsNPC() )
+ {
+ titan.SetNoTarget( true )
+ titan.EnableNPCFlag( NPC_IGNORE_ALL )
+ }
+
+ AddCinematicFlag( player, CE_FLAG_HIDE_MAIN_HUD )
+ AddCinematicFlag( player, CE_FLAG_TITAN_3P_CAM )
+
+ mover.SetOrigin( player.CameraPosition() )
+ mover.SetAngles( player.CameraAngles() )
+ player.SetViewEntity( camera, true )
+
+ player.SetPredictionEnabled( false )
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ player.SetPredictionEnabled( true )
+ }
+ )
+
+ while ( Time() - startTime < moveTime )
+ {
+ if ( hasTitanCameraAttachment )
+ {
+ int titanCameraAttachmentID = titan.LookupAttachment( titanCameraAttachment )
+ viewPos = titan.GetAttachmentOrigin( titanCameraAttachmentID )
+ viewAngles = titan.GetAttachmentAngles( titanCameraAttachmentID )
+ }
+ mover.NonPhysicsMoveTo( viewPos, moveTime - (Time() - startTime), 0, 0 )
+ mover.NonPhysicsRotateTo( viewAngles, moveTime - (Time() - startTime), 0, 0 )
+ wait 0.1
+ }
+
+ if ( hasTitanCameraAttachment )
+ {
+ mover.SetParent( titan, titanCameraAttachment, false, blendTime )
+ }
+
+ wait 0.5
+
+ int tagID = titan.LookupAttachment( "CHESTFOCUS" )
+ while ( Flag( "BossTitanViewFollow" ) )
+ {
+ vector lookVec = Normalize( titan.GetAttachmentOrigin( tagID ) - mover.GetOrigin() )
+ vector angles = VectorToAngles( lookVec )
+ if ( !hasTitanCameraAttachment )
+ mover.NonPhysicsRotateTo( angles, 0.2, 0.0, 0.0 )
+ WaitFrame()
+ }
+
+ // Slam Zoom Out
+
+ mover.ClearParent()
+
+ startTime = Time()
+ while ( Time() - startTime < slamZoomTime )
+ {
+ moverStartPos = player.GetOrigin() - camFeetDiff
+ moverStartAng = FlattenAngles( player.GetAngles() )
+ mover.NonPhysicsMoveTo( moverStartPos, slamZoomTime - (Time() - startTime), 0, 0 )
+ mover.NonPhysicsRotateTo( moverStartAng, slamZoomTime - (Time() - startTime), 0, 0 )
+ wait 0.1
+ }
+
+ // mover.NonPhysicsMoveTo( moverStartPos, slamZoomTime, slamZoomTimeDecel, slamZoomTimeAccel )
+ // mover.NonPhysicsRotateTo( moverStartAng, slamZoomTime, slamZoomTimeDecel, slamZoomTimeAccel )
+ // wait slamZoomTime
+
+ ClearPlayerAnimViewEntity( player )
+ player.SnapEyeAngles( moverStartAng )
+ DeployAndEnableWeapons( player )
+ player.MakeVisible()
+
+ EmitSoundOnEntity( player, "UI_Lobby_RankChip_Disable" )
+}
+
+void function OnBossTitanDamaged( entity titan, var damageInfo )
+{
+}
+
+void function OnBossTitanLostSegment( entity titan, entity attacker )
+{
+ if ( !titan.IsNPC() || !BossTitanVDUEnabled( titan ) )
+ return
+
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( player == attacker || IsMercTitan( titan ) )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanLostSegment", titan.GetEncodedEHandle(), GetTitanCurrentRegenTab( titan ) )
+ }
+}
+
+void function OnBossTitanPrimaryFire( entity titan )
+{
+}
+
+bool function IsVDUTitan( entity titan )
+{
+ Assert( IsSingleplayer() )
+
+ if ( titan.GetTeam() != TEAM_IMC )
+ return false
+
+ switch ( titan.ai.bossTitanType )
+ {
+ case TITAN_AUTO:
+ case TITAN_WEAK:
+ return false
+
+ case TITAN_HENCH:
+ case TITAN_MERC:
+ case TITAN_BOSS:
+ return true
+ }
+
+ Assert( 0, "Unknown boss titan type " + titan.ai.bossTitanType )
+ unreachable
+}
+
+bool function IsBossTitan( entity titan )
+{
+ Assert( IsSingleplayer() )
+
+ if ( titan.GetTeam() != TEAM_IMC )
+ return false
+
+ switch ( titan.ai.bossTitanType )
+ {
+ case TITAN_MERC:
+ case TITAN_BOSS:
+ return true
+ }
+
+ return false
+}
+
+int function GetMercCharacterID( entity titan )
+{
+ return titan.ai.mercCharacterID
+}
+
+asset function GetBossTitanCharacterModel( entity titan )
+{
+ int mercCharacterID = GetMercCharacterID( titan )
+ return GetMercCharacterModel( mercCharacterID )
+}
+
+void function OnTitanLostSegment( entity titan, entity attacker )
+{
+ entity player
+
+ if ( !titan.IsPlayer() )
+ player = titan.GetBossPlayer()
+ else
+ player = titan
+
+ if ( !IsValid( player ) )
+ return
+
+ if ( !IsValid( attacker ) )
+ return
+
+ if ( !attacker.IsNPC() || !IsVDUTitan( attacker ) || !BossTitanVDUEnabled( attacker ) )
+ return
+
+ Remote_CallFunction_NonReplay( player, "BossTitanPlayerLostHealthSegment", GetSegmentHealthForTitan( titan ) )
+}
+
+void function BossTitanRetreat( entity titan )
+{
+ if ( !IsVDUTitan( titan ) || !BossTitanVDUEnabled( titan ) )
+ return
+
+ foreach ( player in GetPlayerArray() )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanRetreat", titan.GetEncodedEHandle() )
+ }
+}
+
+void function BossTitanAdvance( entity titan )
+{
+ if ( !IsVDUTitan( titan ) || !BossTitanVDUEnabled( titan ) )
+ return
+
+ foreach ( player in GetPlayerArray() )
+ {
+ Remote_CallFunction_NonReplay( player, "ServerCallback_BossTitanAdvance", titan.GetEncodedEHandle() )
+ }
+}
+
+/*
+------------------------------------------------------------
+Low Health Titans
+------------------------------------------------------------
+*/
+
+void function MakeLowHealthTitan( entity ent )
+{
+ entity soul = ent.GetTitanSoul()
+ soul.soul.regensHealth = false
+ thread SetHealthValuesForLowHealth( soul )
+ //ent.SetValidHealthBarTarget( false )
+
+ ent.TakeOffhandWeapon( OFFHAND_ORDNANCE )
+ ent.TakeOffhandWeapon( OFFHAND_ANTIRODEO )
+ ent.TakeOffhandWeapon( OFFHAND_EQUIPMENT )
+ ent.TakeOffhandWeapon( OFFHAND_SPECIAL )
+}
+
+void function MakeMidHealthTitan( entity ent )
+{
+ entity soul = ent.GetTitanSoul()
+ soul.soul.regensHealth = false
+ thread SetHealthValuesForMidHealth( soul )
+}
+
+void function SetHealthValuesForMidHealth( entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ WaitEndFrame() // wait for a bunch of variables to start up
+ soul.Signal( SIGNAL_TITAN_HEALTH_REGEN )
+ soul.Signal( "StopShieldRegen" )
+ soul.SetShieldHealth( 0 )
+
+ entity titan = soul.GetTitan()
+ int numSegments = ( titan.GetMaxHealth() / GetSegmentHealthForTitan( titan ) ) - 2
+ Assert( numSegments > 0 )
+ SetSoulBatteryCount( soul, numSegments )
+ if ( IsAlive( titan ) )
+ {
+ soul.soul.skipDoomState = true
+ int segmentHealth = GetSegmentHealthForTitan( titan ) * numSegments
+ titan.SetMaxHealth( segmentHealth )
+ titan.SetHealth( segmentHealth )
+ titan.kv.healthEvalMultiplier = 2
+ }
+
+ titan.Signal( "WeakTitanHealthInitialized" )
+
+ ApplyTitanDamageState( titan )
+}
+
+void function SetHealthValuesForLowHealth( entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ WaitEndFrame() // wait for a bunch of variables to start up
+ soul.Signal( SIGNAL_TITAN_HEALTH_REGEN )
+ soul.Signal( "StopShieldRegen" )
+ soul.SetShieldHealth( 0 )
+
+ int numSegments = 2
+
+ SetSoulBatteryCount( soul, numSegments )
+ entity titan = soul.GetTitan()
+ if ( IsAlive( titan ) )
+ {
+ soul.soul.skipDoomState = true
+ int segmentHealth = GetSegmentHealthForTitan( titan ) * numSegments
+ titan.SetMaxHealth( segmentHealth )
+ titan.SetHealth( segmentHealth )
+ titan.kv.healthEvalMultiplier = 2
+ }
+
+ titan.Signal( "WeakTitanHealthInitialized" )
+
+ ApplyTitanDamageState( titan )
+}
+
+void function ApplyTitanDamageState( entity titan )
+{
+ array<float> healthScale = [
+ 1.0,
+ 0.6,
+ 0.3,
+ 0.1
+ ]
+
+ int state = 0
+
+ if ( titan.HasKey( "DamageState" ) )
+ {
+ state = int( titan.GetValueForKey( "DamageState" ) )
+ }
+
+ titan.SetHealth( titan.GetMaxHealth() * healthScale[state] )
+
+ if ( state >= 1 )
+ {
+ string part = [
+ "left_arm",
+ "right_arm"
+ ].getrandom()
+ GibBodyPart( titan, part )
+ }
+
+ if ( state >= 2 )
+ GibBodyPart( titan, "torso" )
+}
+
+bool function IsMercTitan( entity titan )
+{
+ if ( IsMultiplayer() )
+ return false
+ if ( titan.GetTeam() != TEAM_IMC )
+ return false
+ return titan.ai.bossTitanType == TITAN_MERC
+}
+
+bool function BossTitanVDUEnabled( entity titan )
+{
+ return titan.ai.bossTitanVDUEnabled
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_chatter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_chatter.gnut
new file mode 100644
index 000000000..0429895b1
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_chatter.gnut
@@ -0,0 +1,129 @@
+global function DialogueChatter_Init
+
+global function TitanVO_AlertTitansIfTargetWasKilled
+global function TitanVO_TellPlayersThatAreAlsoFightingThisTarget
+global function TitanVO_AlertTitansTargetingThisTitanOfRodeo
+global function TitanVO_DelayedTitanDown
+
+const TITAN_VO_DIST_SQR = 2000 * 2000
+
+const CHATTER_TIME_LAPSE = 30.0
+//const CHATTER_TIME_LAPSE = 5.0 //For testing
+//const CHATTER_TIME_LAPSE = 8.0 //For testing
+//const CHATTER_TIME_LAPSE = 15.0 //For testing
+
+void function DialogueChatter_Init()
+{
+}
+
+void function TitanVO_TellPlayersThatAreAlsoFightingThisTarget( entity attacker, entity soul )
+{
+ int voEnum
+ if ( attacker.IsTitan() )
+ voEnum = eTitanVO.FRIENDLY_TITAN_HELPING
+ else
+ voEnum = eTitanVO.PILOT_HELPING
+
+ bool atackerIsTitan = attacker.IsTitan()
+ int attackerTeam = attacker.GetTeam()
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ if ( !player.IsTitan() )
+ continue
+
+ if ( player.GetTeam() != attackerTeam )
+ continue
+ // attacker gets a score callout
+ if ( player == attacker )
+ continue
+
+ if ( soul != player.p.currentTargetPlayerOrSoul_Ent )
+ continue
+
+ float timeDif = Time() - player.p.currentTargetPlayerOrSoul_LastHitTime
+ if ( timeDif > CURRENT_TARGET_FORGET_TIME )
+ continue
+
+ // alert other player that cared about this target
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", voEnum )
+ }
+}
+
+void function TitanVO_AlertTitansTargetingThisTitanOfRodeo( entity rodeoer, entity soul )
+{
+ int team = rodeoer.GetTeam()
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ if ( !player.IsTitan() )
+ continue
+
+ if ( player.GetTeam() != team )
+ continue
+
+ if ( soul != player.p.currentTargetPlayerOrSoul_Ent )
+ continue
+
+ // if we havent hurt the target recently then forget about it
+ if ( Time() - player.p.currentTargetPlayerOrSoul_LastHitTime > CURRENT_TARGET_FORGET_TIME )
+ continue
+
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", eTitanVO.FRIENDLY_RODEOING_ENEMY )
+ }
+}
+
+void function TitanVO_DelayedTitanDown( entity ent )
+{
+ vector titanOrigin = ent.GetOrigin()
+ int team = ent.GetTeam()
+
+ wait 0.9
+
+ array<entity> playerArray = GetPlayerArray()
+ float dist = TITAN_VO_DIST_SQR
+
+ foreach ( player in playerArray )
+ {
+ // only titans get BB vo
+ if ( !player.IsTitan() )
+ continue
+
+ if ( DistanceSqr( titanOrigin, player.GetOrigin() ) > dist )
+ continue
+
+ if ( player.GetTeam() != team )
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", eTitanVO.ENEMY_TITAN_DEAD )
+ else
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", eTitanVO.FRIENDLY_TITAN_DEAD )
+ }
+}
+
+
+void function TitanVO_AlertTitansIfTargetWasKilled( entity victim, entity attacker )
+{
+ array<entity> enemyPlayers = GetPlayerArrayOfEnemies( victim.GetTeam() )
+
+ if ( victim.IsTitan() )
+ victim = victim.GetTitanSoul()
+
+ foreach ( player in enemyPlayers )
+ {
+ if ( !player.IsTitan() )
+ continue
+
+ // attacker gets a score callout
+ if ( player == attacker )
+ continue
+
+ if ( victim != player.p.currentTargetPlayerOrSoul_Ent )
+ continue
+
+ if ( Time() - player.p.currentTargetPlayerOrSoul_LastHitTime > CURRENT_TARGET_FORGET_TIME )
+ continue
+
+ // alert other player that cared about this target
+ Remote_CallFunction_Replay( player, "SCB_TitanDialogue", eTitanVO.ENEMY_TARGET_ELIMINATED )
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_cloak_drone.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_cloak_drone.gnut
new file mode 100644
index 000000000..e3addf812
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_cloak_drone.gnut
@@ -0,0 +1,678 @@
+untyped
+
+global function CloakDrone_Init
+
+global function SpawnCloakDrone
+global function GetNPCCloakedDrones
+global function RemoveLeftoverCloakedDrones
+const FX_DRONE_CLOAK_BEAM = $"P_drone_cloak_beam"
+
+const float CLOAK_DRONE_REACHED_HARVESTER_DIST = 1300.0
+
+struct
+{
+ int cloakedDronesManagedEntArrayID
+ table<entity,string> cloakedDroneClaimedSquadList
+} file
+
+struct CloakDronePath
+{
+ vector start
+ vector goal
+ bool goalValid = false
+ float lastHeight
+}
+
+function CloakDrone_Init()
+{
+ PrecacheParticleSystem( FX_DRONE_CLOAK_BEAM )
+
+ file.cloakedDronesManagedEntArrayID = CreateScriptManagedEntArray()
+
+ RegisterSignal( "DroneCleanup" )
+ RegisterSignal( "DroneCrashing" )
+}
+
+entity function SpawnCloakDrone( int team, vector origin, vector angles, vector towerOrigin )
+{
+ int droneCount = GetNPCCloakedDrones().len()
+
+ // add some minor randomness to the spawn location as well as an offset based on number of drones in the world.
+ origin += < RandomIntRange( -64, 64 ), RandomIntRange( -64, 64 ), 300 + (droneCount * 128) >
+
+ entity cloakedDrone = CreateGenericDrone( team, origin, angles )
+ SetSpawnOption_AISettings( cloakedDrone, "npc_drone_cloaked" )
+
+ //these enable global damage callbacks for the cloakedDrone
+ cloakedDrone.s.isHidden <- false
+ cloakedDrone.s.fx <- null
+ cloakedDrone.s.towerOrigin <- towerOrigin
+
+ DispatchSpawn( cloakedDrone )
+ SetTeam( cloakedDrone, team )
+ SetTargetName( cloakedDrone, "Cloak Drone" )
+ cloakedDrone.SetTitle( "#NPC_CLOAK_DRONE" )
+ cloakedDrone.SetMaxHealth( 250 )
+ cloakedDrone.SetHealth( 250 )
+ cloakedDrone.SetTakeDamageType( DAMAGE_YES )
+ cloakedDrone.SetDamageNotifications( true )
+ cloakedDrone.SetDeathNotifications( true )
+ cloakedDrone.Solid()
+ cloakedDrone.Show()
+ cloakedDrone.EnableNPCFlag( NPC_IGNORE_ALL )
+
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_HOVER_LOOP_SFX )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_LOOPING_SFX )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_WARP_IN_SFX )
+
+ cloakedDrone.s.fx = CreateDroneCloakBeam( cloakedDrone )
+
+ SetVisibleEntitiesInConeQueriableEnabled( cloakedDrone, true )
+
+ thread CloakedDronePathThink( cloakedDrone )
+ thread CloakedDroneCloakThink( cloakedDrone )
+
+ #if R1_VGUI_MINIMAP
+ cloakedDrone.Minimap_SetDefaultMaterial( $"vgui/hud/cloak_drone_minimap_orange" )
+ #endif
+ cloakedDrone.Minimap_SetAlignUpright( true )
+ cloakedDrone.Minimap_AlwaysShow( TEAM_IMC, null )
+ cloakedDrone.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ cloakedDrone.Minimap_SetObjectScale( MINIMAP_CLOAKED_DRONE_SCALE )
+ cloakedDrone.Minimap_SetZOrder( MINIMAP_Z_NPC )
+
+ ShowName( cloakedDrone )
+
+ AddToGlobalCloakedDroneList( cloakedDrone )
+ return cloakedDrone
+}
+
+function AddToGlobalCloakedDroneList( cloakedDrone )
+{
+ AddToScriptManagedEntArray( file.cloakedDronesManagedEntArrayID, cloakedDrone )
+}
+
+array<entity> function GetNPCCloakedDrones()
+{
+ return GetScriptManagedEntArray( file.cloakedDronesManagedEntArrayID )
+}
+
+function RemoveLeftoverCloakedDrones()
+{
+ array<entity> droneArray = GetNPCCloakedDrones()
+ foreach ( cloakedDrone in droneArray )
+ {
+ thread CloakedDroneWarpOutAndDestroy( cloakedDrone )
+ }
+}
+
+void function CloakedDroneWarpOutAndDestroy( entity cloakedDrone )
+{
+ cloakedDrone.EndSignal( "OnDestroy" )
+ cloakedDrone.EndSignal( "OnDeath" )
+ cloakedDrone.SetInvulnerable()
+
+ CloakedDroneWarpOut( cloakedDrone, cloakedDrone.GetOrigin() )
+ cloakedDrone.Destroy()
+}
+
+/************************************************************************************************\
+
+ ###### ## ####### ### ## ## #### ## ## ######
+## ## ## ## ## ## ## ## ## ## ### ## ## ##
+## ## ## ## ## ## ## ## ## #### ## ##
+## ## ## ## ## ## ##### ## ## ## ## ## ####
+## ## ## ## ######### ## ## ## ## #### ## ##
+## ## ## ## ## ## ## ## ## ## ## ### ## ##
+ ###### ######## ####### ## ## ## ## #### ## ## ######
+
+\************************************************************************************************/
+//HACK - this should probably move into code
+function CloakedDroneCloakThink( cloakedDrone )
+{
+ expect entity( cloakedDrone )
+
+ cloakedDrone.EndSignal( "OnDestroy" )
+ cloakedDrone.EndSignal( "OnDeath" )
+ cloakedDrone.EndSignal( "DroneCrashing" )
+ cloakedDrone.EndSignal( "DroneCleanup" )
+
+ wait 2 // wait a few seconds since it would start cloaking before picking an npc to follow
+ // some npcs might not be picked since they where already cloaked by accident.
+
+ CloakerThink( cloakedDrone, 400.0, [ "any" ], < 0, 0, -350 >, CloakDroneShouldCloakGuy, 1.5 )
+}
+
+function CloakDroneShouldCloakGuy( cloakedDrone, guy )
+{
+ expect entity( guy )
+ if ( !( guy.IsTitan() || IsSpectre( guy ) || IsGrunt( guy ) || IsSuperSpectre( guy ) ) )
+ return false
+
+ if ( guy.GetTargetName() == "empTitan" )
+ return false
+
+ if ( IsSniperSpectre( guy ) )
+ return false
+
+ if ( IsValid( GetRodeoPilot( guy ) ) )
+ return false
+
+ if ( cloakedDrone.s.isHidden )
+ return false
+
+ if ( StatusEffect_Get( guy, eStatusEffect.sonar_detected ) )
+ return false
+
+ // if ( !cloakedDrone.CanSee( guy ) )
+ // return false
+
+ return true
+}
+
+/************************************************************************************************\
+
+######## ### ######## ## ## #### ## ## ######
+## ## ## ## ## ## ## ## ### ## ## ##
+## ## ## ## ## ## ## ## #### ## ##
+######## ## ## ## ######### ## ## ## ## ## ####
+## ######### ## ## ## ## ## #### ## ##
+## ## ## ## ## ## ## ## ### ## ##
+## ## ## ## ## ## #### ## ## ######
+
+\************************************************************************************************/
+//HACK -> this should probably move into code
+const VALIDPATHFRAC = 0.99
+
+void function CloakedDronePathThink( entity cloakedDrone )
+{
+ cloakedDrone.EndSignal( "OnDestroy" )
+ cloakedDrone.EndSignal( "OnDeath" )
+ cloakedDrone.EndSignal( "DroneCrashing" )
+ cloakedDrone.EndSignal( "DroneCleanup" )
+
+ entity goalNPC = null
+ entity previousNPC = null
+ vector spawnOrigin = cloakedDrone.GetOrigin()
+ vector lastOrigin = cloakedDrone.GetOrigin()
+ float stuckDistSqr = 64.0*64.0
+ float targetLostTime = Time()
+ array<entity> claimedGuys = []
+
+ while( 1 )
+ {
+ while( goalNPC == null )
+ {
+ wait 1.0
+ array<entity> testArray = GetNPCArrayEx( "any", cloakedDrone.GetTeam(), TEAM_ANY, < 0, 0, 0 >, -1 )
+
+ // remove guys already being followed by an cloakedDrone
+ // or in other ways not suitable
+ array<entity> NPCs = []
+ foreach ( guy in testArray )
+ {
+ if ( !IsAlive( guy ) )
+ continue
+
+ //Only cloak titans, spectres, grunts,
+ if ( !( guy.IsTitan() || IsSpectre( guy ) || IsGrunt( guy ) || IsSuperSpectre( guy ) ) )
+ continue
+
+ //Don't cloak arc titans
+ if ( guy.GetTargetName() == "empTitan" )
+ continue
+
+ if ( IsSniperSpectre( guy ) )
+ continue
+
+ if ( IsFragDrone( guy ) )
+ continue
+
+ if ( guy == previousNPC )
+ continue
+
+ if ( guy.ContextAction_IsBusy() )
+ continue
+
+ if ( guy.GetParent() != null )
+ continue
+
+ if ( IsCloaked( guy ) )
+ continue
+
+ if ( IsSquadCenterClose( guy ) == false )
+ continue
+
+ if ( "cloakedDrone" in guy.s && IsAlive( expect entity( guy.s.cloakedDrone ) ) )
+ continue
+
+ if ( CloakedDroneIsSquadClaimed( expect string( guy.kv.squadname ) ) )
+ continue
+
+ if ( IsValid( GetRodeoPilot( guy ) ) )
+ continue
+
+ if ( StatusEffect_Get( guy, eStatusEffect.sonar_detected ) )
+ continue
+
+ NPCs.append( guy )
+ }
+
+ if ( NPCs.len() == 0 )
+ {
+ previousNPC = null
+
+ if ( Time() - targetLostTime > 10 )
+ {
+ // couldn't find anything to cloak for 10 seconds so we'll warp out until we find something
+ if ( cloakedDrone.s.isHidden == false )
+ CloakedDroneWarpOut( cloakedDrone, spawnOrigin )
+ }
+ continue
+ }
+
+ goalNPC = FindBestCloakTarget( NPCs, cloakedDrone.GetOrigin(), cloakedDrone )
+ Assert( goalNPC )
+ }
+
+ CloakedDroneClaimSquad( cloakedDrone, expect string( goalNPC.kv.squadname ) )
+
+ waitthread CloakedDronePathFollowNPC( cloakedDrone, goalNPC )
+
+ CloakedDroneReleaseSquad( cloakedDrone )
+
+ previousNPC = goalNPC
+ goalNPC = null
+ targetLostTime = Time()
+
+ float distSqr = DistanceSqr( lastOrigin, cloakedDrone.GetOrigin() )
+ if ( distSqr < stuckDistSqr )
+ CloakedDroneWarpOut( cloakedDrone, spawnOrigin )
+
+ lastOrigin = cloakedDrone.GetOrigin()
+ }
+}
+
+void function CloakedDroneClaimSquad( entity cloakedDrone, string squadname )
+{
+ if ( GetNPCSquadSize( squadname ) )
+ file.cloakedDroneClaimedSquadList[ cloakedDrone ] <- squadname
+}
+
+void function CloakedDroneReleaseSquad( entity cloakedDrone )
+{
+ if ( cloakedDrone in file.cloakedDroneClaimedSquadList )
+ delete file.cloakedDroneClaimedSquadList[ cloakedDrone ]
+}
+
+bool function CloakedDroneIsSquadClaimed( string squadname )
+{
+ table<entity,string> cloneTable = clone file.cloakedDroneClaimedSquadList
+ foreach ( entity cloakedDrone, squad in cloneTable )
+ {
+ if ( !IsAlive( cloakedDrone ) )
+ delete file.cloakedDroneClaimedSquadList[ cloakedDrone ]
+ else if ( squad == squadname )
+ return true
+ }
+ return false
+}
+
+void function CloakedDronePathFollowNPC( entity cloakedDrone, entity goalNPC )
+{
+ cloakedDrone.EndSignal( "OnDestroy" )
+ cloakedDrone.EndSignal( "OnDeath" )
+ cloakedDrone.EndSignal( "DroneCrashing" )
+ goalNPC.EndSignal( "OnDeath" )
+ goalNPC.EndSignal( "OnDestroy" )
+
+ if ( !( "cloakedDrone" in goalNPC.s ) )
+ goalNPC.s.cloakedDrone <- null
+ goalNPC.s.cloakedDrone = cloakedDrone
+
+ OnThreadEnd(
+ function() : ( goalNPC )
+ {
+ if ( IsAlive( goalNPC ) )
+ goalNPC.s.cloakedDrone = null
+ }
+ )
+
+ int droneTeam = cloakedDrone.GetTeam()
+
+ //vector maxs = < 64, 64, 53.5 >//bigger than model to compensate for large effect
+ //vector mins = < -64, -64, -64 >
+
+ vector maxs = < 32, 32, 32 >//bigger than model to compensate for large effect
+ vector mins = < -32, -32, -32 >
+
+ int mask = cloakedDrone.GetPhysicsSolidMask()
+
+ float defaultHeight = 300
+ array<float> traceHeightsLow = [ -75.0, -150.0, -250.0 ]
+ array<float> traceHeightsHigh = [ 150.0, 300.0, 800.0, 1500.0 ]
+
+ float waitTime = 0.25
+
+ CloakDronePath path
+ path.goalValid = false
+ path.lastHeight = defaultHeight
+
+ //If drone is following titan wait for titan to leave bubble shield.
+ if ( goalNPC.IsTitan() )
+ WaitTillHotDropComplete( goalNPC )
+
+ while( goalNPC.GetTeam() == droneTeam )
+ {
+ if ( IsValid( GetRodeoPilot( goalNPC ) ) )
+ return
+
+ //If our target npc gets revealed by a sonar pulse, ditch that chump.
+ if ( StatusEffect_Get( goalNPC, eStatusEffect.sonar_detected ) )
+ return
+
+ float minDist = CLOAK_DRONE_REACHED_HARVESTER_DIST * CLOAK_DRONE_REACHED_HARVESTER_DIST
+ float distToGenerator = DistanceSqr( goalNPC.GetOrigin(), cloakedDrone.s.towerOrigin )
+ //if we've gotten our npc to the generator, go find someone farther out to escort.
+ if ( distToGenerator <= minDist )
+ return
+
+ //DebugDrawCircleOnEnt( goalNPC, 20, 255, 0, 0, 0.1 )
+
+ float startTime = Time()
+ path.goalValid = false
+
+ CloakedDroneFindPathDefault( path, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+
+ //find a new path if necessary
+ if ( !path.goalValid )
+ {
+ //lets check some heights and see if any are valid
+ CloakedDroneFindPathHorizontal( path, traceHeightsLow, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+
+ if ( !path.goalValid )
+ {
+ //OK so no way to directly go to those heights - lets see if we can move vertically down,
+ CloakedDroneFindPathVertical( path, traceHeightsLow, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+
+ if ( !path.goalValid )
+ {
+ //still no good...lets check up
+ CloakedDroneFindPathHorizontal( path, traceHeightsHigh, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+
+ if ( !path.goalValid )
+ {
+ //no direct shots up - lets try moving vertically up first
+ CloakedDroneFindPathVertical( path, traceHeightsHigh, defaultHeight, mins, maxs, cloakedDrone, goalNPC, mask )
+ }
+ }
+ }
+ }
+
+ // if we can't find a valid path find a new goal
+ if ( !path.goalValid )
+ {
+ waitthread CloakedDroneWarpOut( cloakedDrone, GetCloakTargetOrigin( goalNPC ) + < 0, 0, defaultHeight > )
+ CloakedDroneWarpIn( cloakedDrone, GetCloakTargetOrigin( goalNPC ) + < 0, 0, defaultHeight > )
+ continue
+ }
+
+ if ( cloakedDrone.s.isHidden == true )
+ CloakedDroneWarpIn( cloakedDrone, cloakedDrone.GetOrigin() )
+
+ thread AssaultOrigin( cloakedDrone, path.goal )
+
+ float endTime = Time()
+ float elapsedTime = endTime - startTime
+ if ( elapsedTime < waitTime )
+ wait waitTime - elapsedTime
+ }
+}
+
+bool function CloakedDroneFindPathDefault( CloakDronePath path, float defaultHeight, vector mins, vector maxs, entity cloakedDrone, entity goalNPC, int mask )
+{
+ vector offset = < 0, 0, defaultHeight >
+ path.start = ( cloakedDrone.GetOrigin() ) + < 0, 0, 32 > //Offset so path start is just above drone instead at bottom of drone.
+ path.goal = GetCloakTargetOrigin( goalNPC ) + offset
+
+ //find out if we can get there using the default height
+ TraceResults result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ] , mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( path.start, path.goal, 50, 0, 0, true, 1.0 )
+ if ( result.fraction >= VALIDPATHFRAC )
+ {
+ path.lastHeight = defaultHeight
+ path.goalValid = true
+ }
+
+ return path.goalValid
+}
+
+bool function CloakedDroneFindPathHorizontal( CloakDronePath path, array<float> traceHeights, float defaultHeight, vector mins, vector maxs, entity cloakedDrone, entity goalNPC, int mask )
+{
+ wait 0.1
+
+ vector offset
+ float testHeight
+
+ //slight optimization... recheck if the last time was also not the default height
+ if ( path.lastHeight != defaultHeight )
+ {
+ offset = < 0, 0, defaultHeight + path.lastHeight >
+ path.start = ( cloakedDrone.GetOrigin() )
+ path.goal = GetCloakTargetOrigin( goalNPC ) + offset
+
+ TraceResults result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ], mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( path.start, path.goal, 0, 255, 0, true, 1.0 )
+ if ( result.fraction >= VALIDPATHFRAC )
+ {
+ path.goalValid = true
+ return path.goalValid
+ }
+ }
+
+ for ( int i = 0; i < traceHeights.len(); i++ )
+ {
+ testHeight = traceHeights[ i ]
+ if ( path.lastHeight == testHeight )
+ continue
+
+// wait 0.1
+
+ offset = < 0, 0, defaultHeight + testHeight >
+ path.start = ( cloakedDrone.GetOrigin() ) + ( testHeight > 0 ? < 0, 0, 0 > : < 0, 0, 32 > ) //Check from the top or bottom of the drone depending on if the drone is going up or down
+ path.goal = GetCloakTargetOrigin( goalNPC ) + offset
+
+ TraceResults result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ], mask, TRACE_COLLISION_GROUP_NONE )
+ if ( result.fraction < VALIDPATHFRAC )
+ {
+ //DebugDrawLine( path.start, path.goal, 200, 0, 0, true, 3.0 )
+ continue
+ }
+
+ //DebugDrawLine( path.start, path.goal, 0, 255, 0, true, 3.0 )
+
+ path.lastHeight = testHeight
+ path.goalValid = true
+ break
+ }
+
+ return path.goalValid
+}
+
+bool function CloakedDroneFindPathVertical( CloakDronePath path, array<float> traceHeights, float defaultHeight, vector mins, vector maxs, entity cloakedDrone, entity goalNPC, int mask )
+{
+ vector offset
+ vector origin
+ float testHeight
+
+ for ( int i = 0; i < traceHeights.len(); i++ )
+ {
+ wait 0.1
+
+ testHeight = traceHeights[ i ]
+ origin = cloakedDrone.GetOrigin()
+ offset = < 0, 0, defaultHeight + testHeight >
+ path.start = < origin.x, origin.y, defaultHeight + testHeight >
+ path.goal = GetCloakTargetOrigin( goalNPC ) + offset
+
+ TraceResults result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ], mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( path.start, path.goal, 50, 50, 100, true, 1.0 )
+ if ( result.fraction < VALIDPATHFRAC )
+ continue
+
+ //ok so it's valid - lets see if we can move to it from where we are
+// wait 0.1
+
+ path.goal = < path.start.x, path.start.y, path.start.z >
+ path.start = cloakedDrone.GetOrigin()
+
+ result = TraceHull( path.start, path.goal, mins, maxs, [ cloakedDrone, goalNPC ], mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( path.start, path.goal, 255, 255, 0, true, 1.0 )
+ if ( result.fraction < VALIDPATHFRAC )
+ continue
+
+ path.lastHeight = testHeight
+ path.goalValid = true
+ break
+ }
+
+ return path.goalValid
+}
+
+void function CloakedDroneWarpOut( entity cloakedDrone, vector origin )
+{
+ if ( cloakedDrone.s.isHidden == false )
+ {
+ // only do this if we are not already hidden
+ FadeOutSoundOnEntity( cloakedDrone, CLOAKED_DRONE_LOOPING_SFX, 0.5 )
+ FadeOutSoundOnEntity( cloakedDrone, CLOAKED_DRONE_HOVER_LOOP_SFX, 0.5 )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_WARP_OUT_SFX )
+
+ cloakedDrone.s.fx.Fire( "StopPlayEndCap" )
+ cloakedDrone.SetTitle( "" )
+ cloakedDrone.s.isHidden = true
+ cloakedDrone.NotSolid()
+ cloakedDrone.Minimap_Hide( TEAM_IMC, null )
+ cloakedDrone.Minimap_Hide( TEAM_MILITIA, null )
+ cloakedDrone.SetNoTarget( true )
+ // let the beam fx end
+
+ if ( "smokeEffect" in cloakedDrone.s )
+ {
+ cloakedDrone.s.smokeEffect.Kill_Deprecated_UseDestroyInstead()
+ delete cloakedDrone.s.smokeEffect
+ }
+ UntrackAllToneMarks( cloakedDrone )
+
+ wait 0.3 // wait a bit before hidding the done so that the fx looks better
+ cloakedDrone.Hide()
+ }
+
+ wait 2.0
+
+ cloakedDrone.DisableBehavior( "Follow" )
+ thread AssaultOrigin( cloakedDrone, origin )
+ cloakedDrone.SetOrigin( origin )
+}
+
+void function CloakedDroneWarpIn( entity cloakedDrone, vector origin )
+{
+ cloakedDrone.DisableBehavior( "Follow" )
+ cloakedDrone.SetOrigin( origin )
+ PutEntityInSafeSpot( cloakedDrone, cloakedDrone, null, cloakedDrone.GetOrigin() + <0, 0, 32>, cloakedDrone.GetOrigin() )
+ thread AssaultOrigin( cloakedDrone, origin )
+
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_HOVER_LOOP_SFX )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_LOOPING_SFX )
+ EmitSoundOnEntity( cloakedDrone, CLOAKED_DRONE_WARP_IN_SFX )
+
+ cloakedDrone.Show()
+ cloakedDrone.s.fx.Fire( "start" )
+ cloakedDrone.SetTitle( "#NPC_CLOAK_DRONE" )
+ cloakedDrone.s.isHidden = false
+ cloakedDrone.Solid()
+ cloakedDrone.Minimap_AlwaysShow( TEAM_IMC, null )
+ cloakedDrone.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ cloakedDrone.SetNoTarget( false )
+}
+
+
+entity function CreateDroneCloakBeam( entity cloakedDrone )
+{
+ entity fx = PlayLoopFXOnEntity( FX_DRONE_CLOAK_BEAM, cloakedDrone, "", null, < 90, 0, 0 > )//, visibilityFlagOverride = null, visibilityFlagEntOverride = null )
+ return fx
+}
+
+entity function FindBestCloakTarget( array<entity> npcArray, vector origin, entity drone )
+{
+ entity selectedNPC = null
+ float maxDist = 10000 * 10000
+ float minDist = 1300 * 1300
+ float highestScore = -1
+
+ foreach ( npc in npcArray )
+ {
+ float score = 0
+ float distToGenerator = DistanceSqr( npc.GetOrigin(), drone.s.towerOrigin )
+ if ( distToGenerator > minDist )
+ {
+ // only give dist bonus if we aren't to close to the generator.
+ local dist = DistanceSqr( npc.GetOrigin(), origin )
+ score = GraphCapped( dist, maxDist, minDist, 0, 1 )
+ }
+
+ if ( npc.IsTitan() )
+ {
+ score += 0.75
+ if ( IsArcTitan( npc ) )
+ score -= 0.1
+ if ( IsMortarTitan( npc ) )
+ score -= 0.2
+// if ( IsNukeTitan( npc ) )
+// score += 0.1
+ }
+ if ( score > highestScore )
+ {
+ highestScore = score
+ selectedNPC = npc
+ }
+ }
+
+ return selectedNPC
+}
+
+vector function GetCloakTargetOrigin( entity npc )
+{
+ // returns the center of squad if the npc is in one
+ // else returns a good spot to cloak a titan
+
+ vector origin
+
+ if ( GetNPCSquadSize( npc.kv.squadname ) == 0 )
+ {
+ origin = npc.GetOrigin() + npc.GetNPCVelocity()
+ }
+ else
+ origin = npc.GetSquadCentroid()
+
+ Assert( origin.x < ( 16384 * 100 ) );
+
+ // defensive hack
+ if ( origin.x > ( 16384 * 100 ) )
+ origin = npc.GetOrigin()
+
+ return origin
+}
+
+function IsSquadCenterClose( npc, dist = 256 )
+{
+ // return true if there is no squad
+ if ( GetNPCSquadSize( npc.kv.squadname ) == 0 )
+ return true
+
+ // return true if the squad isn't too spread out.
+ if ( DistanceSqr( npc.GetSquadCentroid(), npc.GetOrigin() ) <= ( dist * dist ) )
+ return true
+
+ return false
+}
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()
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_emp_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_emp_titans.gnut
new file mode 100644
index 000000000..638166c83
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_emp_titans.gnut
@@ -0,0 +1,181 @@
+untyped
+
+global function EmpTitans_Init
+
+global function EMPTitanThinkConstant
+
+const DAMAGE_AGAINST_TITANS = 150
+const DAMAGE_AGAINST_PILOTS = 40
+
+const EMP_DAMAGE_TICK_RATE = 0.3
+const FX_EMP_FIELD = $"P_xo_emp_field"
+const FX_EMP_FIELD_1P = $"P_body_emp_1P"
+
+function EmpTitans_Init()
+{
+ AddDamageCallbackSourceID( eDamageSourceId.titanEmpField, EmpField_DamagedEntity )
+ PrecacheParticleSystem( FX_EMP_FIELD )
+ PrecacheParticleSystem( FX_EMP_FIELD_1P )
+
+ RegisterSignal( "StopEMPField" )
+}
+
+void function EMPTitanThinkConstant( entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "Doomed" )
+ titan.EndSignal( "StopEMPField" )
+
+ //We don't want pilots accidently rodeoing an electrified titan.
+ DisableTitanRodeo( titan )
+
+ //Used to identify this titan as an arc titan
+ SetTargetName( titan, "empTitan" )
+
+ //Wait for titan to stand up and exit bubble shield before deploying arc ability.
+ WaitTillHotDropComplete( titan )
+
+ if ( HasSoul( titan ) )
+ {
+ entity soul = titan.GetTitanSoul()
+ soul.EndSignal( "StopEMPField" )
+ }
+
+ local attachment = GetEMPAttachmentForTitan( titan )
+
+ local attachID = titan.LookupAttachment( attachment )
+
+ EmitSoundOnEntity( titan, "EMP_Titan_Electrical_Field" )
+
+ array<entity> particles = []
+
+ //emp field fx
+ vector origin = titan.GetAttachmentOrigin( attachID )
+ if ( titan.IsPlayer() )
+ {
+ entity particleSystem = CreateEntity( "info_particle_system" )
+ particleSystem.kv.start_active = 1
+ particleSystem.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ particleSystem.SetValueForEffectNameKey( FX_EMP_FIELD_1P )
+
+ particleSystem.SetOrigin( origin )
+ particleSystem.SetOwner( titan )
+ DispatchSpawn( particleSystem )
+ particleSystem.SetParent( titan, GetEMPAttachmentForTitan( titan ) )
+ particles.append( particleSystem )
+ }
+
+ entity particleSystem = CreateEntity( "info_particle_system" )
+ particleSystem.kv.start_active = 1
+ if ( titan.IsPlayer() )
+ particleSystem.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // everyone but owner
+ else
+ particleSystem.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ particleSystem.SetValueForEffectNameKey( FX_EMP_FIELD )
+ particleSystem.SetOwner( titan )
+ particleSystem.SetOrigin( origin )
+ DispatchSpawn( particleSystem )
+ particleSystem.SetParent( titan, GetEMPAttachmentForTitan( titan ) )
+ particles.append( particleSystem )
+
+ titan.SetDangerousAreaRadius( ARC_TITAN_EMP_FIELD_RADIUS )
+
+ OnThreadEnd(
+ function () : ( titan, particles )
+ {
+ if ( IsValid( titan ) )
+ {
+ StopSoundOnEntity( titan, "EMP_Titan_Electrical_Field" )
+ EnableTitanRodeo( titan ) //Make the arc titan rodeoable now that it is no longer electrified.
+ }
+
+ foreach ( particleSystem in particles )
+ {
+ if ( IsValid_ThisFrame( particleSystem ) )
+ {
+ particleSystem.ClearParent()
+ particleSystem.Fire( "StopPlayEndCap" )
+ particleSystem.Kill_Deprecated_UseDestroyInstead( 1.0 )
+ }
+ }
+ }
+ )
+
+ wait RandomFloat( EMP_DAMAGE_TICK_RATE )
+
+ while ( true )
+ {
+ origin = titan.GetAttachmentOrigin( attachID )
+
+ RadiusDamage(
+ origin, // center
+ titan, // attacker
+ titan, // inflictor
+ DAMAGE_AGAINST_PILOTS, // damage
+ DAMAGE_AGAINST_TITANS, // damageHeavyArmor
+ ARC_TITAN_EMP_FIELD_INNER_RADIUS, // innerRadius
+ ARC_TITAN_EMP_FIELD_RADIUS, // outerRadius
+ SF_ENVEXPLOSION_NO_DAMAGEOWNER, // flags
+ 0, // distanceFromAttacker
+ DAMAGE_AGAINST_PILOTS, // explosionForce
+ DF_ELECTRICAL | DF_STOPS_TITAN_REGEN, // scriptDamageFlags
+ eDamageSourceId.titanEmpField ) // scriptDamageSourceIdentifier
+
+ wait EMP_DAMAGE_TICK_RATE
+ }
+}
+
+void function EmpField_DamagedEntity( entity target, var damageInfo )
+{
+ if ( !IsAlive( target ) )
+ return
+
+ entity titan = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !IsValid( titan ) )
+ return
+
+ local className = target.GetClassName()
+ if ( className == "rpg_missile" || className == "npc_turret_sentry" || className == "grenade" )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ if ( DamageInfo_GetDamage( damageInfo ) <= 0 )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ if ( target.IsPlayer() )
+ {
+ if ( !titan.IsPlayer() && IsArcTitan( titan ) )
+ {
+ if ( !titan.s.electrocutedPlayers.contains( target ) )
+ titan.s.electrocutedPlayers.append( target )
+ }
+
+ const ARC_TITAN_SCREEN_EFFECTS = 0.085
+ const ARC_TITAN_EMP_DURATION = 0.35
+ const ARC_TITAN_EMP_FADEOUT_DURATION = 0.35
+
+ local attachID = titan.LookupAttachment( "hijack" )
+ local origin = titan.GetAttachmentOrigin( attachID )
+ local distSqr = DistanceSqr( origin, target.GetOrigin() )
+
+ local minDist = ARC_TITAN_EMP_FIELD_INNER_RADIUS_SQR
+ local maxDist = ARC_TITAN_EMP_FIELD_RADIUS_SQR
+ local empFxHigh = ARC_TITAN_SCREEN_EFFECTS
+ local empFxLow = ( ARC_TITAN_SCREEN_EFFECTS * 0.6 )
+ float screenEffectAmplitude = GraphCapped( distSqr, minDist, maxDist, empFxHigh, empFxLow )
+
+ StatusEffect_AddTimed( target, eStatusEffect.emp, screenEffectAmplitude, ARC_TITAN_EMP_DURATION, ARC_TITAN_EMP_FADEOUT_DURATION )
+ }
+}
+
+string function GetEMPAttachmentForTitan( entity titan )
+{
+ return "hijack"
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_gunship.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_gunship.gnut
new file mode 100644
index 000000000..2f1fdc96f
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_gunship.gnut
@@ -0,0 +1,97 @@
+untyped
+
+global function AiGunship_Init
+
+global function GunshipThink
+
+global const SOUND_GUNSHIP_HOVER = "Gunship_Hover"
+global const SOUND_GUNSHIP_EXPLODE_DEFAULT = "Gunship_Explode"
+global const FX_GUNSHIP_EXPLOSION = $"P_veh_exp_crow"
+
+function AiGunship_Init()
+{
+ PrecacheParticleSystem( FX_GUNSHIP_EXPLOSION )
+ AddDeathCallback( "npc_gunship", GunshipDeath )
+}
+
+
+function GunshipThink( gunship )
+{
+ gunship.EndSignal( "OnDeath" )
+
+ entity owner
+ entity currentTarget
+ local accuracyMultiplierBase = gunship.kv.AccuracyMultiplier
+ local accuracyMultiplierAgainstDrones = 100
+
+ while( true )
+ {
+ wait 0.25
+
+ //----------------------------------
+ // Get owner and current enemy
+ //----------------------------------
+ currentTarget = expect entity( gunship.GetEnemy() )
+ owner = expect entity( gunship.GetFollowTarget() )
+
+ //----------------------------------
+ // Free roam if owner is dead or HasEnemy
+ //----------------------------------
+ if ( ( !IsAlive( owner ) ) || ( currentTarget != null ) )
+ {
+ gunship.DisableBehavior( "Follow" )
+ }
+
+ //---------------------------------------------------------------------
+ // If owner is alive and no enemies in sight, go back and follow owner
+ //----------------------------------------------------------------------
+ if ( ( IsAlive( owner ) ) && ( currentTarget == null ) )
+ {
+ gunship.EnableBehavior( "Follow" )
+ }
+
+
+ //----------------------------------------------
+ // Jack up accuracy if targeting a small target (like a drone)
+ //----------------------------------------------
+ if ( ( currentTarget != null ) && ( IsAirDrone( currentTarget ) ) )
+ {
+ gunship.kv.AccuracyMultiplier = accuracyMultiplierAgainstDrones
+ }
+ else
+ {
+ gunship.kv.AccuracyMultiplier = accuracyMultiplierBase
+ }
+ }
+
+}
+
+
+void function GunshipDeath( entity gunship, var damageInfo )
+{
+ /*
+ Script errors
+
+ // Explosion effect
+ entity explosion = CreateEntity( "info_particle_system" )
+ explosion.SetOrigin( gunship.GetWorldSpaceCenter() )
+ explosion.SetAngles( gunship.GetAngles() )
+ explosion.SetValueForEffectNameKey( FX_GUNSHIP_EXPLOSION )
+ explosion.kv.start_active = 1
+ DispatchSpawn( explosion )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, gunship.GetOrigin(), SOUND_GUNSHIP_EXPLODE_DEFAULT )
+ explosion.destroy( 3 )
+
+ gunship.Destroy()
+
+ P_veh_exp_hornet, TAG_ORIGIN, attach
+
+ */
+
+ //TEMP
+ PlayFX( FX_GUNSHIP_EXPLOSION, gunship.GetOrigin() )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, gunship.GetOrigin(), "Goblin_Dropship_Explode" )
+ gunship.Destroy()
+}
+
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_lethality.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_lethality.gnut
new file mode 100644
index 000000000..771fe6d93
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_lethality.gnut
@@ -0,0 +1,97 @@
+untyped
+
+global enum eAILethality
+{
+ VeryLow,
+ Low,
+ Medium,
+ High,
+ VeryHigh
+}
+
+global function SetAILethality
+
+global function UpdateNPCForAILethality
+
+function SetAILethality( aiLethality )
+{
+ Assert( IsMultiplayer() )
+ level.nv.aiLethality = aiLethality
+
+ switch ( aiLethality )
+ {
+ case eAILethality.Medium:
+ break
+
+ case eAILethality.High:
+ NPCSetAimConeFocusParams( 6, 2.5 )
+ NPCSetAimPatternFocusParams( 4, 0.3, 0.8 )
+ break
+ case eAILethality.VeryHigh:
+ NPCSetAimConeFocusParams( 5, 2.0 )
+ NPCSetAimPatternFocusParams( 4, 0.3, 0.8 )
+ break
+ }
+
+ // reset ai lethality
+
+ array<entity> npcs = GetNPCArray()
+ foreach ( npc in npcs )
+ {
+ UpdateNPCForAILethality( npc )
+ }
+}
+
+
+function SetTitanAccuracyAndProficiency( entity npcTitan )
+{
+ Assert( IsMultiplayer() )
+ int lethality = Riff_AILethality()
+ float accuracyMultiplier = 1.0
+ int weaponProficiency = eWeaponProficiency.GOOD
+
+ entity player = GetPetTitanOwner( npcTitan )
+ entity soul = npcTitan.GetTitanSoul()
+
+ // auto titans have lower proficiency
+ if ( player && soul == null)
+ {
+ soul = player.GetTitanSoul() // in mid transfer
+ }
+
+ if ( IsValid( soul ) )
+ {
+ if ( SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ {
+ weaponProficiency = eWeaponProficiency.GOOD
+ }
+ else if ( player )
+ {
+ weaponProficiency = eWeaponProficiency.AVERAGE
+ entity ordnanceWeapon = npcTitan.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ if ( IsValid( ordnanceWeapon ) )
+ ordnanceWeapon.AllowUse( false )
+
+ entity centerWeapon = npcTitan.GetOffhandWeapon( OFFHAND_TITAN_CENTER )
+ if ( IsValid( centerWeapon ) )
+ centerWeapon.AllowUse( false )
+ }
+ }
+
+ npcTitan.kv.AccuracyMultiplier = accuracyMultiplier
+ npcTitan.kv.WeaponProficiency = weaponProficiency
+}
+
+function UpdateNPCForAILethality( entity npc )
+{
+ Assert( IsMultiplayer() )
+ if ( npc.IsTitan() )
+ {
+ SetTitanAccuracyAndProficiency( npc )
+ return
+ }
+
+ if ( IsMinion( npc ) )
+ SetProficiency( npc )
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_faces.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_faces.gnut
new file mode 100644
index 000000000..e6d3bcf0a
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_faces.gnut
@@ -0,0 +1,226 @@
+untyped
+
+global function MarvinFaces_Init
+
+global function MarvinFace
+global function MarvinThinksAwhile
+global function MarvinFaceExists
+global function SetMarvinBodyType
+global function MarvinSetFace
+
+function MarvinFaces_Init()
+{
+
+ RegisterSignal( "StopThinking" )
+
+ AddSpawnCallback( "npc_marvin", MarvinSpawnCallback )
+
+ SetupMarvinFaces()
+}
+
+function SetupMarvinFaces()
+{
+ // setup marvin face mappings
+ level.marvinFaces <- {}
+ level.marvinFaces[ MARVIN_TYPE_WORKER ] <-
+ {
+ none = 0
+ happy = 1
+ sad = 2
+ angry = 3
+ think1 = 4
+ think2 = 5
+ question = 6
+ }
+
+ // Use the yellow worker marvin skins since shooters are from SP and shooter value = 0 which is LevelEd's default.
+ // Real shooter skins are 7-13. If we want real skins, we can add them here and adjust the marvin spawn points per level.
+ level.marvinFaces[ MARVIN_TYPE_SHOOTER ] <-
+ {
+ none = 0
+ happy = 1
+ sad = 2
+ angry = 3
+ think1 = 4
+ think2 = 5
+ question = 6
+ }
+
+ level.marvinFaces[ MARVIN_TYPE_FIREFIGHTER ] <-
+ {
+ none = 14
+ happy = 15
+ sad = 16
+ angry = 17
+ think1 = 18
+ think2 = 19
+ question = 20
+ }
+
+ // No idea what this type of marvin is...legacy stuff. Just make them yellow.
+ level.marvinFaces[ MARVIN_TYPE_MARVINONE ] <-
+ {
+ none = 0
+ happy = 1
+ sad = 2
+ angry = 3
+ think1 = 4
+ think2 = 5
+ question = 6
+ }
+
+// Nothing uses this except debug statements that are commented out
+/*
+ level.marvinFaceNames <- {}
+ // invert map for tests
+ foreach ( key, val in level.marvinFace )
+ {
+ level.marvinFaceNames[ val ] <- key
+ }
+*/
+}
+
+void function MarvinFace( entity marvin )
+{
+ thread MarvinFaceThink( marvin )
+}
+
+void function MarvinFaceThink( entity marvin )
+{
+ //printl( "Setting up marvin face for " + marvin )
+ for ( ;; )
+ {
+ waitthread MarvinUndamagedFacePicker( marvin )
+
+// printl( "damaged " + marvin )
+ if ( !IsAlive( marvin ) )
+ break
+
+ waitthread MarvinWounded( marvin )
+
+ if ( !IsAlive( marvin ) )
+ break
+ }
+
+ if ( IsValid_ThisFrame( marvin ) )
+ MarvinSetFace( marvin, "none" )
+}
+
+function MarvinWounded( marvin )
+{
+ marvin.EndSignal( "OnDeath" )
+ MarvinSetFace( marvin, "sad" )
+ wait 2.3
+ waitthread MarvinThinksAwhile( marvin, RandomFloatRange( 2, 4 ) )
+}
+
+void function EntSignals( entity ent, string signal )
+{
+ if ( IsValid_ThisFrame( ent ) )
+ ent.Signal( signal )
+}
+
+function MarvinThinksAwhile( marvin, time )
+{
+ expect entity( marvin )
+
+ marvin.EndSignal( "StopThinking" )
+ delaythread( time ) EntSignals( marvin, "StopThinking" )
+
+ // think for a bit
+ for ( ;; )
+ {
+ MarvinSetFace( marvin, "think1" )
+ wait 0.4
+ MarvinSetFace( marvin, "think2" )
+ wait 0.4
+ }
+}
+
+function MarvinUndamagedFacePicker( marvin )
+{
+ marvin.EndSignal( "OnDeath" )
+ marvin.EndSignal( "OnDamaged" )
+ local i
+
+ for ( ;; )
+ {
+ if ( !marvin.GetEnemy() )
+ {
+ MarvinSetFace( marvin, "happy" )
+ marvin.WaitSignal( "OnFoundEnemy" )
+ }
+
+ waitthread MarvinThinksAwhile( marvin, RandomFloatRange( 2, 4 ) )
+
+ if ( marvin.GetEnemy() )
+ {
+ MarvinSetFace( marvin, "angry" )
+ marvin.WaitSignal( "OnLostEnemy" )
+ }
+ }
+}
+
+function MarvinSetFace( self, face )
+{
+// printl( self + " got face " + face )
+ Assert( MarvinFaceExists( self, face ), "No face " + face + " in level.marvinFace" )
+
+ //prin( "Changing " + self + " face from " + level.marvinFaceNames[ skin ] + " to " + face )
+ self.SetSkin( GetMarvinFace( self, face ) )
+ self.Signal( "StopThinking" )
+}
+
+function MarvinFaceExists( npc_marvin, face )
+{
+ local marvinType = GetMarvinBodyType( npc_marvin )
+
+ if ( marvinType in level.marvinFaces )
+ return true
+
+// return ( face in level.marvinFaces[ marvinType ] )
+}
+
+function GetMarvinFace( npc_marvin, face )
+{
+ local marvinType = GetMarvinBodyType( npc_marvin )
+
+ Assert( MarvinFaceExists( npc_marvin, face ), "No face " + face + " in level.marvinFace" )
+
+ local faceID = level.marvinFaces[ marvinType ][ face ]
+
+ return faceID
+}
+
+function SetMarvinBodyType( npc_marvin )
+{
+ if( "bodytype" in npc_marvin.s )
+ {
+ Assert( npc_marvin.s.bodytype >= MARVIN_TYPE_SHOOTER && npc_marvin.s.bodytype <= MARVIN_TYPE_FIREFIGHTER, "Specified invalid body type index " + npc_marvin.s.bodytype + ", Use values from 0-2 instead." )
+
+ switch( npc_marvin.s.bodytype )
+ {
+ case MARVIN_TYPE_FIREFIGHTER:
+ local index = npc_marvin.FindBodyGroup( "firefighter" )
+ local state = 1
+ npc_marvin.SetBodygroup( index, state )
+ break
+ }
+ }
+}
+
+function GetMarvinBodyType( npc_marvin )
+{
+ local bodyType = MARVIN_TYPE_WORKER
+
+ if( "bodytype" in npc_marvin.s )
+ bodyType = npc_marvin.s.bodytype
+
+ return bodyType
+}
+
+void function MarvinSpawnCallback( entity npc_marvin )
+{
+ SetMarvinBodyType( npc_marvin )
+ npc_marvin.SetDeathNotifications( true ) //Primarily so we can do HandleDeathPackage for Marvins. Can just add a deathcallback if this is too expensive
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_jobs.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_jobs.gnut
new file mode 100644
index 000000000..588b4d75e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvin_jobs.gnut
@@ -0,0 +1,600 @@
+
+/*
+ ToDo:
+ -if marvin has no jobs to go to make him to back to spawn position instead of standing at last node
+*/
+
+global function MarvinJobs_Init
+global function MarvinJobThink
+global function GetMarvinType
+
+const DEBUG_MARVIN_JOBS = false
+const MAX_JOB_SEARCH_DIST_SQR = 1000 * 1000
+const JOB_NODE_COOLDOWN_TIME = 15.0
+
+struct MarvinJob
+{
+ string validMarvinType
+ entity node
+ entity user
+ string jobType
+ bool tempJob
+ float nextUsableTime = 0
+ entity barrel
+}
+
+struct
+{
+ array<MarvinJob> marvinJobs
+ table<string,void functionref( entity,MarvinJob)> jobFunctions
+} file
+
+
+
+
+// ██╗███╗ ██╗██╗████████╗
+// ██║████╗ ██║██║╚══██╔══╝
+// ██║██╔██╗ ██║██║ ██║
+// ██║██║╚██╗██║██║ ██║
+// ██║██║ ╚████║██║ ██║
+// ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝
+
+void function MarvinJobs_Init()
+{
+ file.jobFunctions[ "welding" ] <- SimpleJobAnims
+ file.jobFunctions[ "welding_under" ] <- SimpleJobAnims
+ file.jobFunctions[ "window" ] <- SimpleJobAnims
+ file.jobFunctions[ "fightFire" ] <- SimpleJobAnims
+ file.jobFunctions[ "barrel_pickup" ] <- MarvinPicksUpBarrel
+ file.jobFunctions[ "barrel_putdown" ] <- MarvinPutsDownBarrel
+ file.jobFunctions[ "repair_over_edge" ] <- SimpleJobAnims
+ file.jobFunctions[ "repair_above" ] <- SimpleJobAnims
+ file.jobFunctions[ "repair_under" ] <- SimpleJobAnims
+ file.jobFunctions[ "datacards" ] <- SimpleJobAnims
+
+ file.jobFunctions[ "drone_welding" ] <- SimpleJobAnims
+ file.jobFunctions[ "drone_inspect" ] <- SimpleJobAnims
+
+ RegisterSignal( "pickup_barrel" )
+ RegisterSignal( "putdown_barrel" )
+ RegisterSignal( "JobStarted" )
+ RegisterSignal( "StopDoingJobs" )
+
+ AddSpawnCallback( "script_marvin_job", InitMarvinJob )
+
+ AddCallback_EntitiesDidLoad( MarvinJobsEntitiesDidLoad )
+}
+
+void function InitMarvinJob( entity node )
+{
+ Assert( node.HasKey( "job" ) )
+ Assert( node.kv.job != "" )
+ Assert( string( node.kv.job ) in file.jobFunctions, "Marvin job node at " + node.GetOrigin() + " has unhandled job type " + string( node.kv.job ) )
+ string editorClass = GetEditorClass( node )
+
+ // Drop node to ground for certain types or if checked on the entity
+ if ( editorClass == "" )
+ {
+ if ( !node.HasKey( "hover" ) || node.kv.hover != "1" )
+ DropToGround( node )
+ }
+
+ if ( DEBUG_MARVIN_JOBS )
+ DebugDrawAngles( node.GetOrigin(), node.GetAngles() )
+
+ // Create marvin job struct
+ MarvinJob marvinJob
+ marvinJob.node = node
+ marvinJob.jobType = string( node.kv.job )
+ marvinJob.tempJob = node.HasKey( "tempJob" ) && node.kv.tempJob == "1"
+
+ if ( marvinJob.jobType == "barrel_pickup" )
+ marvinJob.barrel = CreateBarrel( node )
+
+ // Set what marvin_type of NPC can use this job
+ switch ( editorClass )
+ {
+ case "script_marvin_drone_job":
+ marvinJob.validMarvinType = "marvin_type_drone"
+ break
+ default:
+ marvinJob.validMarvinType = "marvin_type_walker"
+ break
+ }
+
+ file.marvinJobs.append( marvinJob )
+}
+
+void function MarvinJobsEntitiesDidLoad()
+{
+ if ( DEBUG_MARVIN_JOBS )
+ DebugMarvinJobs()
+}
+
+
+
+
+
+// ████████╗██╗ ██╗██╗███╗ ██╗██╗ ██╗
+// ╚══██╔══╝██║ ██║██║████╗ ██║██║ ██╔╝
+// ██║ ███████║██║██╔██╗ ██║█████╔╝
+// ██║ ██╔══██║██║██║╚██╗██║██╔═██╗
+// ██║ ██║ ██║██║██║ ╚████║██║ ██╗
+// ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝
+
+void function MarvinJobThink( entity marvin )
+{
+ EndSignal( marvin, "OnDeath" )
+ EndSignal( marvin, "OnDestroy" )
+ EndSignal( marvin, "StopDoingJobs" )
+
+ // Wait a frame because npcs that are spawned at map load may run this function before job nodes are finished being initialized
+ WaitFrame()
+
+ // Get all jobs this marvin can do
+ array<MarvinJob> jobs = GetJobsForMarvin( marvin )
+ if ( jobs.len() == 0 )
+ return
+
+ OnThreadEnd(
+ function() : ( marvin )
+ {
+ Assert( !IsAlive( marvin ), "MarvinJobThink ended but the marvin is still alive" )
+ }
+ )
+
+ while ( true )
+ {
+ foreach ( MarvinJob job in jobs )
+ {
+ waitthread MarvinDoJob( marvin, job )
+ WaitFrame()
+ }
+
+ jobs.randomize()
+ WaitFrame()
+ }
+}
+
+void function MarvinDoJob( entity marvin, MarvinJob job )
+{
+ Assert( IsAlive( marvin ), "Marvin " + marvin + " is not alive" )
+ EndSignal( marvin, "OnFailedToPath" )
+ EndSignal( marvin, "OnDeath" )
+
+ // Don't do a job that's already in use or not ready to be used again
+ if ( IsValid( job.user ) || Time() < job.nextUsableTime )
+ return
+
+ // Don't use a barrel put down job if you can'r carrying a barrel
+ if ( job.jobType == "barrel_putdown" && !IsValid( marvin.ai.carryBarrel ) )
+ return
+
+ // If you're carrying a barrel, only do a barrel put down job
+ if ( IsValid( marvin.ai.carryBarrel ) && job.jobType != "barrel_putdown" )
+ return
+
+ OnThreadEnd(
+ function() : ( job )
+ {
+ job.user = null
+ job.nextUsableTime = Time() + JOB_NODE_COOLDOWN_TIME
+ }
+ )
+
+ // Default walk anim
+ MarvinDefaultMoveAnim( marvin )
+
+ // Node gets occupied
+ job.user = marvin
+
+ if ( DEBUG_MARVIN_JOBS )
+ DebugDrawLine( marvin.GetWorldSpaceCenter(), job.node.GetOrigin(), 255, 0, 0, true, 3.0 )
+
+ // Run the job function
+ thread DontDisableJobOnPathFailOrDeath( marvin, job )
+ waitthread file.jobFunctions[ job.jobType ]( marvin, job )
+ if ( IsValid( marvin ) )
+ marvin.Anim_Stop()
+}
+
+void function DontDisableJobOnPathFailOrDeath( entity marvin, MarvinJob job )
+{
+ EndSignal( marvin, "JobStarted" )
+ WaitSignal( marvin, "OnFailedToPath", "OnDeath" )
+ job.nextUsableTime = Time()
+}
+
+
+
+
+
+// ██╗ ██████╗ ██████╗ ███████╗██╗ ██╗███╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗███████╗
+// ██║██╔═══██╗██╔══██╗ ██╔════╝██║ ██║████╗ ██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝
+// ██║██║ ██║██████╔╝ █████╗ ██║ ██║██╔██╗ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║███████╗
+// ██ ██║██║ ██║██╔══██╗ ██╔══╝ ██║ ██║██║╚██╗██║██║ ██║ ██║██║ ██║██║╚██╗██║╚════██║
+// ╚█████╔╝╚██████╔╝██████╔╝ ██║ ╚██████╔╝██║ ╚████║╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║███████║
+// ╚════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
+
+void function SimpleJobAnims( entity marvin, MarvinJob job )
+{
+ // Get the anims to use for the job
+ array<string> anims
+ switch ( job.jobType )
+ {
+ // Marvin jobs
+ case "welding":
+ anims.append( "mv_idle_weld" )
+ break
+ case "welding_under":
+ anims.append( "mv_weld_under" )
+ anims.append( "mv_weld_under" )
+ anims.append( "mv_weld_under_stumble" )
+ break
+ case "window":
+ anims.append( "mv_idle_wash_window_noloop" )
+ anims.append( "mv_idle_buff_window_noloop" )
+ break
+ case "fightFire":
+ anims.append( "mv_fireman_idle" )
+ anims.append( "mv_fireman_shift" )
+ break
+ case "repair_over_edge":
+ anims.append( "mv_repair_overedge" )
+ anims.append( "mv_repair_overedge" )
+ anims.append( "mv_repair_overedge_stumble" )
+ break
+ case "repair_above":
+ anims.append( "mv_repair_ship_above" )
+ break
+ case "repair_under":
+ anims.append( "mv_repair_under" )
+ anims.append( "mv_repair_under_stumble" )
+ break
+ case "datacards":
+ anims.append( "mv_job_replace_datacards" )
+ break
+
+ // Marvin drone jobs
+ case "drone_welding":
+ anims.append( "dw_jobs_welding_wallpanel" )
+ break
+ case "drone_inspect":
+ anims.append( "inspect1" )
+ anims.append( "inspect2" )
+ break
+ }
+ Assert( anims.len() > 0 )
+
+ if ( IsMarvinWalker( marvin ) )
+ waitthread MarvinRunToAnimStart( marvin, anims[0], job.node )
+ else
+ waitthread MarvinFlyToAnimStart( marvin, anims[0], job.node )
+
+ Signal( marvin, "JobStarted" )
+
+ while ( true )
+ {
+ anims.randomize()
+ foreach ( string anim in anims )
+ {
+ float animLength = marvin.GetSequenceDuration( anim ) // wait anim length because some anims may be looping so we can't wait for them to end
+
+ if ( IsMarvinDrone( marvin ) )
+ thread PlayAnimTeleport( marvin, anim, job.node )
+ else
+ thread PlayAnim( marvin, anim, job.node, null, 0.6 )
+
+ wait animLength
+ }
+ if ( job.tempJob )
+ break
+ }
+}
+
+void function MarvinPicksUpBarrel( entity marvin, MarvinJob job )
+{
+ // Don't try to pick up a barrel if there isn't one nearby
+ if ( !IsValid( job.barrel ) )
+ return
+ if ( Distance( job.node.GetOrigin(), job.barrel.GetOrigin() ) > 25 )
+ return
+
+ EndSignal( job.barrel, "OnDestroy" )
+
+ entity info_target = CreateEntity( "info_target" )
+ DispatchSpawn( info_target )
+
+ OnThreadEnd(
+ function () : ( info_target )
+ {
+ info_target.Destroy()
+ }
+ )
+
+ vector barrelFlatAngles = job.barrel.GetAngles()
+ barrelFlatAngles.x = 0
+ barrelFlatAngles.z = 0
+
+ info_target.SetOrigin( job.barrel.GetOrigin() )
+ info_target.SetAngles( barrelFlatAngles )
+
+ DropToGround( info_target )
+
+ if ( info_target.GetOrigin().z < -MAX_WORLD_COORD )
+ return // Fell through map
+
+ if ( DEBUG_MARVIN_JOBS )
+ thread DrawAnglesForMovingEnt( info_target, 30.0 )
+
+
+ // Go to the barrel
+ MarvinRunToAnimStart( marvin, "mv_carry_barrel_pickup", info_target )
+
+ // Try to pick it up
+ thread PlayAnim( marvin, "mv_carry_barrel_pickup", info_target, null, 0.6 )
+
+ // Wait until animation should pick up the barrel
+ marvin.WaitSignal( "pickup_barrel" )
+
+ // Get attachment info
+ string attachment = "PROPGUN"
+ int attachIndex = marvin.LookupAttachment( attachment )
+ vector attachOrigin = marvin.GetAttachmentOrigin( attachIndex )
+
+ // Make sure the barrel is close when it's time to parent the barrel
+ if ( Distance( attachOrigin, job.barrel.GetOrigin() ) > 25 )
+ {
+ marvin.Anim_Stop()
+ return
+ }
+
+ // Marvin picks up the barrel and carries it
+ thread MarvinCarryBarrel( marvin, job.barrel )
+
+ marvin.WaitSignal( "OnAnimationDone" )
+}
+
+void function MarvinCarryBarrel( entity marvin, entity barrel )
+{
+ marvin.EndSignal( "OnDeath" )
+ marvin.EndSignal( "OnDamaged" )
+ marvin.EndSignal( "putdown_barrel" )
+
+ OnThreadEnd(
+ function () : ( marvin, barrel )
+ {
+ if ( IsValid( barrel ) )
+ {
+ barrel.kv.solid = SOLID_VPHYSICS
+ barrel.ClearParent()
+ barrel.SetOwner( null )
+ EntFireByHandle( barrel, "wake", "", 0, null, null )
+ EntFireByHandle( barrel, "enablemotion", "", 0, null, null )
+ }
+
+ if ( IsAlive( marvin ) )
+ {
+ MarvinDefaultMoveAnim( marvin )
+ marvin.ClearIdleAnim()
+ marvin.ai.carryBarrel = null
+ }
+ }
+ )
+
+ string attachment = "PROPGUN"
+ marvin.SetMoveAnim( "mv_carry_barrel_walk" )
+ marvin.SetIdleAnim( "mv_carry_barrel_idle" )
+ barrel.SetParent( marvin, attachment, false, 0.5 )
+ barrel.SetOwner( marvin )
+
+ barrel.kv.solid = 0 // not solid
+
+ marvin.ai.carryBarrel = barrel
+
+ WaitSignal( marvin, "OnDestroy" )
+}
+
+void function MarvinPutsDownBarrel( entity marvin, MarvinJob job )
+{
+ Assert( IsValid( marvin.ai.carryBarrel ) )
+
+ // Don't place a barrel here if there is already one
+ if ( IsValid( job.barrel ) )
+ {
+ if ( Distance( job.node.GetOrigin(), job.barrel.GetOrigin() ) <= 25 )
+ return
+ }
+
+ EndSignal( marvin.ai.carryBarrel, "OnDestroy" )
+
+ marvin.SetMoveAnim( "mv_carry_barrel_walk" )
+ marvin.SetIdleAnim( "mv_carry_barrel_idle" )
+
+ // Walk to the put down spot
+ MarvinRunToAnimStart( marvin, "mv_carry_barrel_putdown", job.node )
+
+ // Put down the barrel
+ thread PlayAnim( marvin, "mv_carry_barrel_putdown", job.node, null, 0.6 )
+
+ // Wait for release
+ marvin.WaitSignal( "putdown_barrel" )
+
+ marvin.WaitSignal( "OnAnimationDone" )
+}
+
+
+
+
+// ██╗ ██╗████████╗██╗██╗ ██╗████████╗██╗ ██╗
+// ██║ ██║╚══██╔══╝██║██║ ██║╚══██╔══╝╚██╗ ██╔╝
+// ██║ ██║ ██║ ██║██║ ██║ ██║ ╚████╔╝
+// ██║ ██║ ██║ ██║██║ ██║ ██║ ╚██╔╝
+// ╚██████╔╝ ██║ ██║███████╗██║ ██║ ██║
+// ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝
+
+bool function IsMarvinWalker( entity marvin )
+{
+ return GetMarvinType( marvin ) == "marvin_type_walker"
+}
+
+bool function IsMarvinDrone( entity marvin )
+{
+ return GetMarvinType( marvin ) == "marvin_type_drone"
+}
+
+string function GetMarvinType( entity npc )
+{
+ var marvinType = npc.Dev_GetAISettingByKeyField( "marvin_type" )
+ if ( marvinType == null )
+ return "not_marvin"
+
+ return expect string( marvinType )
+}
+
+bool function IsJobNode( entity node )
+{
+ if ( node.GetClassName() == "script_marvin_job" )
+ return true
+ if ( GetEditorClass( node ) == "script_marvin_drone_job" )
+ return true
+ return false
+}
+
+void function MarvinDefaultMoveAnim( entity marvin )
+{
+ if ( IsMarvinWalker( marvin ) )
+ {
+ marvin.SetNPCMoveSpeedScale( 1.0 )
+ marvin.SetMoveAnim( "walk_all" )
+ }
+}
+
+array<MarvinJob> function GetJobsForMarvin( entity marvin )
+{
+ string marvinType = GetMarvinType( marvin )
+
+ // Get jobs this marvin links to, if any, and randomize
+ array<MarvinJob> linkedJobs
+ array<entity> linkedEnts = marvin.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ if ( IsJobNode( ent ) )
+ {
+ MarvinJob linkedJob = GetMarvinJobForNode( ent )
+ Assert( IsValid( linkedJob.node ) )
+
+ // Error if we are linking to the wrong type of job node
+ Assert( marvinType == linkedJob.validMarvinType, "npc_marvin at " + marvin.GetOrigin() + " links to a marvin job of the wrong marvin_type" )
+
+ linkedJobs.append( linkedJob )
+ }
+ }
+ linkedJobs.randomize()
+
+ // If marvin was linked to jobs we only consider those
+ if ( marvin.HasKey( "LinkedJobsOnly" ) && marvin.kv.LinkedJobsOnly == "1" )
+ {
+ Assert( linkedJobs.len() > 0, "marvin at " + marvin.GetOrigin() + " has LinkedJobsOnly marked but does not link to any job nodes" )
+ return linkedJobs
+ }
+
+ // Add all jobs within valid distance and randomize
+ array<MarvinJob> jobs
+ foreach ( MarvinJob marvinJob in file.marvinJobs )
+ {
+ if ( marvinType != marvinJob.validMarvinType )
+ continue
+
+ // Don't re-add a job that was linked to
+ if ( linkedJobs.contains( marvinJob ) )
+ continue
+
+ // Teleport nodes are for special case jobs with no nav mesh do son't consider them automatically
+ if ( marvinJob.node.HasKey( "teleport" ) && marvinJob.node.kv.teleport == "1" )
+ continue
+
+ // Only search for jobs within a max distance
+ if ( DistanceSqr( marvinJob.node.GetOrigin(), marvin.GetOrigin() ) <= MAX_JOB_SEARCH_DIST_SQR )
+ jobs.append( marvinJob )
+ }
+
+ // Randomize the order so the marvin does them out of order
+ jobs.randomize()
+
+ // Add the linked jobs to the list, and put them at the beginning of the priority
+ foreach ( MarvinJob linkedJob in linkedJobs )
+ jobs.insert( 0, linkedJob )
+
+ // Debug draw jobs this marvin can take
+ if ( DEBUG_MARVIN_JOBS )
+ {
+ foreach ( MarvinJob job in jobs )
+ {
+ if ( linkedJobs.contains( job ) )
+ DebugDrawLine( marvin.GetOrigin(), job.node.GetOrigin(), 255, 255, 0, true, 10.0 )
+ else
+ DebugDrawLine( marvin.GetOrigin(), job.node.GetOrigin(), 200, 200, 200, true, 10.0 )
+ }
+ }
+
+ return jobs
+}
+
+void function DebugMarvinJobs()
+{
+ while ( true )
+ {
+ foreach ( MarvinJob marvinJob in file.marvinJobs )
+ {
+ string appendText = "AVAILABLE"
+ float timeTillNextUse = marvinJob.nextUsableTime - Time()
+ if ( IsValid( marvinJob.user ) )
+ appendText = "RESERVED"
+ else if ( timeTillNextUse > 0 )
+ appendText = format( "%.1f", timeTillNextUse )
+ DebugDrawText( marvinJob.node.GetOrigin(), marvinJob.jobType + " (" + appendText + ")", true, 0.1 )
+ }
+ wait 0.05
+ }
+}
+
+MarvinJob function GetMarvinJobForNode( entity node )
+{
+ MarvinJob marvinJob
+ foreach ( MarvinJob marvinJob in file.marvinJobs )
+ {
+ if ( marvinJob.node == node )
+ return marvinJob
+ }
+ return marvinJob
+}
+
+entity function CreateBarrel( entity node )
+{
+ return CreatePropPhysics( node.GetModelName(), node.GetOrigin(), node.GetAngles() )
+}
+
+void function MarvinRunToAnimStart( entity marvin, string anim, entity jobNode )
+{
+ if ( jobNode.HasKey( "teleport" ) && jobNode.kv.teleport == "1" )
+ wait 0.1
+ else
+ RunToAnimStartPos( marvin, anim, jobNode )
+}
+
+void function MarvinFlyToAnimStart( entity marvin, string anim, entity jobNode )
+{
+ if ( jobNode.HasKey( "teleport" ) && jobNode.kv.teleport == "1" )
+ {
+ wait 0.1
+ return
+ }
+
+ AnimRefPoint animStartInfo = marvin.Anim_GetStartForRefPoint( anim, jobNode.GetOrigin(), jobNode.GetAngles() )
+
+ marvin.AssaultPoint( animStartInfo.origin )
+ marvin.AssaultSetAngles( animStartInfo.angles, true )
+ marvin.AssaultSetArrivalTolerance( 16 )
+ marvin.WaitSignal( "OnFinishedAssault" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvins.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvins.gnut
new file mode 100644
index 000000000..fc8b7d1ee
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_marvins.gnut
@@ -0,0 +1,141 @@
+untyped
+
+global function AiMarvins_Init
+
+
+function AiMarvins_Init()
+{
+ FlagInit( "Disable_Marvins" )
+ FlagSet( "Disable_Marvins" )
+
+ level.livingMarvins <- {}
+ AddSpawnCallback( "npc_marvin", LivingMarvinSpawned )
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+}
+
+void function EntitiesDidLoad()
+{
+ if ( IsAutoPopulateEnabled() == false )
+ return
+
+ FlagEnd( "disable_npcs" )
+
+ array<entity> marvin_spawners = GetEntArrayByClass_Expensive( "info_spawnpoint_marvin" )
+
+ if ( !marvin_spawners.len() )
+ return
+
+ for ( ;; )
+ {
+ wait 3
+
+ if ( !Flag( "Disable_Marvins" ) )
+ {
+ if ( TotalLivingMarvins() < 5 )
+ {
+ SpawnRandomMarvin( marvin_spawners )
+ }
+ }
+ }
+}
+
+void function LivingMarvinSpawned( entity self )
+{
+ level.livingMarvins[ self ] <- self
+}
+
+function TotalLivingMarvins()
+{
+ local count = 0
+ foreach ( entity marvin in clone level.livingMarvins )
+ {
+ if ( IsAlive( marvin ) )
+ {
+ count++
+ continue
+ }
+
+ // cleanup dead marvins
+ delete level.livingMarvins[ marvin ]
+ }
+ return count
+}
+
+entity function SpawnRandomMarvin( array<entity> marvin_spawners )
+{
+ marvin_spawners.randomize()
+ entity spawnpoint = marvin_spawners[0] // if no valid spawn is found use this one
+ for ( int i = 0; i < marvin_spawners.len(); i++ )
+ {
+ if ( IsMarvinSpawnpointValid( marvin_spawners[ i ] ) )
+ {
+ spawnpoint = marvin_spawners[ i ]
+ break
+ }
+ }
+
+ entity marvin = SpawnAmbientMarvin( spawnpoint )
+ return marvin
+}
+
+bool function IsMarvinSpawnpointValid( entity spawnpoint )
+{
+ // ensure spawnpoint is not occupied (i.e. would spawn inside another player or object )
+ if ( spawnpoint.IsOccupied() )
+ return false
+
+ bool visible = spawnpoint.IsVisibleToEnemies( TEAM_IMC ) || spawnpoint.IsVisibleToEnemies( TEAM_MILITIA )
+ if ( visible )
+ return false
+
+ return true
+}
+
+entity function SpawnAmbientMarvin( entity spawnpoint )
+{
+ entity npc_marvin = CreateEntity( "npc_marvin" )
+ SetTargetName( npc_marvin, UniqueString( "mp_random_marvin") )
+ npc_marvin.SetOrigin( spawnpoint.GetOrigin() )
+ npc_marvin.SetAngles( spawnpoint.GetAngles() )
+ //npc_marvin.kv.rendercolor = "255 255 255"
+ npc_marvin.kv.health = -1
+ npc_marvin.kv.max_health = -1
+ npc_marvin.kv.spawnflags = 516 // Fall to ground, Fade Corpse
+ //npc_marvin.kv.FieldOfView = 0.5
+ //npc_marvin.kv.FieldOfViewAlert = 0.2
+ npc_marvin.kv.AccuracyMultiplier = 1.0
+ npc_marvin.kv.physdamagescale = 1.0
+ npc_marvin.kv.WeaponProficiency = eWeaponProficiency.GOOD
+
+ Marvin_SetModels( npc_marvin, spawnpoint )
+
+ DispatchSpawn( npc_marvin )
+
+ SetTeam( npc_marvin, TEAM_UNASSIGNED )
+
+ return npc_marvin
+}
+
+function Marvin_SetModels( entity npc_marvin, entity spawnpoint )
+{
+ //default
+ npc_marvin.s.bodytype <- MARVIN_TYPE_WORKER
+
+ // set body and head based on KVP
+ if ( spawnpoint.HasKey( "bodytype" ) )
+ {
+ local bodytype = spawnpoint.GetValueForKey( "bodytype" ).tointeger()
+
+ Assert( bodytype >= MARVIN_TYPE_SHOOTER && bodytype <= MARVIN_TYPE_FIREFIGHTER, "Specified invalid body type index " + bodytype + " for info_spawnpoint_marvin " + spawnpoint + ", Use values from 0-2 instead." )
+
+ npc_marvin.s.bodytype = bodytype
+ }
+
+
+ if ( spawnpoint.HasKey( "headtype" ) )
+ {
+ local headtype = spawnpoint.GetValueForKey( "headtype" )
+ npc_marvin.kv.body = headtype
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_spectres.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_spectres.gnut
new file mode 100644
index 000000000..4aa3ac302
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_spectres.gnut
@@ -0,0 +1,7 @@
+global function MortarSpectreGetSquadFiringPositions
+
+array<vector> function MortarSpectreGetSquadFiringPositions(vector origin, vector testTarget)
+{
+ array< vector > ret
+ return ret
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut
new file mode 100644
index 000000000..08598808a
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut
@@ -0,0 +1,395 @@
+untyped
+
+global function MortarTitanThink
+global function MortarTitans_Init
+
+global function MortarTitanDeathCleanup
+global function MortarMissileFiredCallback
+global function MoveToMortarPosition
+
+global function MortarTitanKneelToAttack
+
+global function MortarTitanAttack
+
+global function MortarTitanStopAttack
+
+//global function MortarAIWaitToEngage
+
+const float MORTAR_TITAN_ABORT_ATTACK_HEALTH_FRAC = 0.90 // will stop mortar attack if he's health gets below 90% of his current health.
+const float MORTAR_TITAN_POSITION_SEARCH_RANGE = 1024 //3072 // How far away from his spawn point a mortar titan will look for positions to mortar from.
+const float MORTAR_TITAN_ENGAGE_DELAY = 3.0 // How long before a mortar titan start to attack the generator if he's taken damage getting to his mortar position.
+const float MORTAR_TITAN_REENGAGE_DELAY = 7.0 // How long before a mortar titan goes back to attacking the generator after breaking of an attack.
+
+// --------------------------------------------------------------------
+// MORTAR TITAN LOGIC
+// --------------------------------------------------------------------
+
+function MortarTitans_Init()
+{
+ RegisterSignal( "InterruptMortarAttack" )
+ RegisterSignal( "BeginMortarAttack" )
+}
+
+void function MortarTitanDeathCleanup( entity titan )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( titan )
+ {
+ entity animEnt = titan.ai.carryBarrel
+
+ if ( IsValid( animEnt ) )
+ animEnt.Destroy()
+
+ if ( IsAlive( titan ) )
+ {
+ titan.Signal( "InterruptMortarAttack" )
+ titan.Anim_Stop()
+ }
+ }
+ )
+
+ WaitForever()
+}
+
+void function MortarMissileFiredCallback( entity missile, entity weaponOwner )
+{
+ thread MortarMissileThink( missile, weaponOwner )
+}
+
+void function MortarMissileThink( entity missile, entity weaponOwner )
+{
+ Assert( IsValid( missile ) )
+
+ missile.EndSignal( "OnDestroy" )
+ missile.EndSignal( "OnDeath" )
+
+ if ( !IsValid( weaponOwner.ai.mortarTarget ) )
+ return
+
+ entity targetEnt = weaponOwner.ai.mortarTarget
+
+ missile.DamageAliveOnly( true )
+ missile.kv.lifetime = 6.0
+ missile.s.mortar <- true
+ vector startPos = missile.GetOrigin()
+
+ // made a hacky way to get the mortar arc to go higher and still have it hit it's target.
+
+ float dist = Distance( startPos, targetEnt.GetOrigin() )
+
+ // radius tightens over time
+ float radius = GraphCapped( Time() - weaponOwner.ai.spawnTime, 60.0, 180.0, 220, 100 )
+ missile.SetMissileTarget( targetEnt, < RandomFloatRange( -radius, radius ), RandomFloatRange( -radius, radius ), 0 > )
+
+ string sound = "weapon_spectremortar_projectile"
+ if ( weaponOwner.IsTitan() )
+ sound = "Weapon_FlightCore_Incoming_Projectile"
+
+ EmitSoundAtPosition( weaponOwner.GetTeam(), targetEnt.GetOrigin(), sound )
+
+ float homingSpeedMin = 10.0
+ float homingSpeedMax = Graph( dist, 2500, 7000, 400, 200 )
+ float estTravelTime = GraphCapped( dist, 0, 7000, 0, 5 )
+
+ float startTime = Time()
+ while( true )
+ {
+ float frac = min( 1, pow( ( Time() - startTime ) / estTravelTime, 2.0 ) )
+
+ if ( frac > 1.0 )
+ break
+
+ float homingSpeed = GraphCapped( frac, 0, 1, homingSpeedMin, homingSpeedMax )
+
+ missile.SetHomingSpeeds( homingSpeed, 0 )
+
+ wait 0.25
+ }
+
+ missile.ClearMissileTargetPosition()
+}
+
+void function MoveToMortarPosition( entity titan, vector origin, entity target )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ titan.SetLookDistOverride( 320 )
+ titan.SetHearingSensitivity( 0 )
+ titan.EnableNPCMoveFlag( NPCMF_PREFER_SPRINT )
+
+ local animEnt = titan.ai.carryBarrel
+
+ local dir = target.GetOrigin() - origin
+ local dist = dir.Norm()
+ local angles = VectorToAngles( dir )
+ angles.x = 0
+ angles.z = 0
+
+ float frac = TraceLineSimple( origin + < 0, 0, 32 >, origin + < 0, 0, -32 >, titan )
+ if ( frac > 0 && frac < 1 )
+ origin = origin + < 0, 0, 32 > - < 0, 0, 64 * frac >
+
+ animEnt.SetOrigin( origin )
+ animEnt.SetAngles( angles )
+
+ float goalRadius = titan.GetMinGoalRadius()
+
+ OnThreadEnd(
+ function() : ( titan )
+ {
+ if ( !IsValid( titan ) )
+ return
+
+ local classname = titan.GetClassName()
+ titan.DisableLookDistOverride()
+ titan.SetHearingSensitivity( 1 )
+ titan.DisableNPCMoveFlag( NPCMF_PREFER_SPRINT )
+ }
+ )
+
+ local tries = 0
+ while( true )
+ {
+ local dist = Distance( titan.GetOrigin(), origin )
+ if ( dist <= goalRadius * 2 )
+ break
+
+ printt( "Mortar titan moving toward his goal", dist, tries++ )
+ titan.AssaultPoint( origin )
+ titan.AssaultSetGoalRadius( goalRadius )
+
+ local result = WaitSignal( titan, "OnFinishedAssault", "OnEnterGoalRadius" )
+ }
+}
+
+void function MortarTitanKneelToAttack( entity titan )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ entity animEnt = titan.ai.carryBarrel
+ waitthread PlayAnim( titan, "at_mortar_stand2knee", animEnt )
+}
+
+function MortarTitanAttack( entity titan, entity target )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "InterruptMortarAttack" )
+
+ OnThreadEnd(
+ function() : ( titan )
+ {
+ if ( !IsValid( titan ) )
+ return
+
+ if ( "selectedPosition" in titan.s )
+ {
+ titan.s.selectedPosition.inUse = false
+ delete titan.s.selectedPosition
+ }
+
+ if ( IsAlive( titan ) )
+ thread MortarTitanAttackEnd( titan )
+ }
+ )
+
+ titan.ai.mortarTarget = target
+ entity animEnt = titan.ai.carryBarrel
+
+ entity weapon = titan.GetActiveWeapon()
+
+ while ( weapon.IsWeaponOffhand() )
+ {
+ WaitFrame()
+ weapon = titan.GetActiveWeapon()
+ }
+
+ weapon.SetMods( [ "coop_mortar_titan" ] )
+
+ while( true )
+ {
+ waitthread PlayAnim( titan, "at_mortar_knee", animEnt )
+ }
+}
+
+function MortarTitanAttackEnd( entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ entity animEnt = titan.ai.carryBarrel
+
+ // remove the mortar mod, we do this so that we don't get mortar sound and fx when firing normal
+ entity weapon = titan.GetActiveWeapon()
+
+ while ( weapon.IsWeaponOffhand() )
+ {
+ WaitFrame()
+ weapon = titan.GetActiveWeapon()
+ }
+
+ weapon.SetMods( [] )
+
+ WaitEndFrame() // if I didn't add this PlayAnim, below, would return immediately for some unknown reason.
+
+ if ( IsValid( animEnt ) && IsAlive( titan ) )
+ waitthread PlayAnim( titan, "at_mortar_knee2stand", animEnt )
+}
+
+function MortarTitanStopAttack( titan )
+{
+ titan.Signal( "InterruptMortarAttack" )
+}
+
+function MortarTitanStopAttack_Internal( titan )
+{
+ titan.Signal( "InterruptMortarAttack" )
+ titan.Anim_Stop()
+}
+
+void function MortarAIWaitToEngage( entity titan, float timeFrame, int minDamage = 75 )
+{
+ entity soul = titan.GetTitanSoul()
+ float endtime = Time() + timeFrame
+ int lastHealth = titan.GetHealth() + soul.GetShieldHealth()
+ float tickTime = 1.0
+
+ while ( Time() < endtime )
+ {
+ wait tickTime
+
+ int currentHealth = titan.GetHealth() + soul.GetShieldHealth()
+ if ( lastHealth > ( currentHealth + minDamage ) ) // add minDamage so that we ignore low amounts of damage.
+ {
+ lastHealth = currentHealth
+ endtime = Time() + timeFrame
+ }
+ }
+}
+
+
+/*******************************************************************\
+ MORTAR TITANS
+\*******************************************************************/
+//Function assumes that given Titan is spawned as npc_titan_atlas_tracker_mortar. Changing the Titan's AISettings post-spawn
+//disrupts the Titan's titanfall animations and can result in the Titan landing outside the level.
+void function MortarTitanThink( entity titan, entity generator )
+{
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ entity soul = titan.GetTitanSoul()
+ soul.EndSignal( "OnDestroy" )
+
+ titan.ai.carryBarrel = CreateScriptRef()
+ titan.TakeWeaponNow( titan.GetActiveWeapon().GetWeaponClassName() )
+ titan.GiveWeapon( "mp_titanweapon_rocketeer_rocketstream" )
+ titan.SetActiveWeaponByName( "mp_titanweapon_rocketeer_rocketstream" )
+ titan.SetScriptName( "mortar_titan" )
+
+ entity weapon = titan.GetActiveWeapon()
+ weapon.w.missileFiredCallback = MortarMissileFiredCallback
+ thread MortarTitanDeathCleanup( titan )
+
+ WaitTillHotDropComplete( titan )
+
+ float minEngagementDuration = 5
+ StationaryAIPosition ornull mortarPosition = GetRandomStationaryPosition( titan.GetOrigin(), MORTAR_TITAN_POSITION_SEARCH_RANGE, eStationaryAIPositionTypes.MORTAR_TITAN )
+ while ( mortarPosition == null )
+ {
+ // incase all stationary titan positions are in use wait for one to become available
+ wait 5
+ mortarPosition = GetRandomStationaryPosition( titan.GetOrigin(), MORTAR_TITAN_POSITION_SEARCH_RANGE, eStationaryAIPositionTypes.MORTAR_TITAN )
+ }
+
+ expect StationaryAIPosition( mortarPosition )
+
+ ClaimStationaryAIPosition( mortarPosition )
+
+ OnThreadEnd(
+ function() : ( mortarPosition )
+ {
+ // release mortar position when dead
+ ReleaseStationaryAIPosition( mortarPosition )
+ }
+ )
+
+ float minDamage = 75 // so that the titan doesn't care about small amounts of damage.
+
+ while( true )
+ {
+ vector origin = mortarPosition.origin
+
+ float startHealth = float( titan.GetHealth() + soul.GetShieldHealth() )
+ waitthread MoveToMortarPosition( titan, origin, generator )
+
+ if ( startHealth > ( ( titan.GetHealth() + soul.GetShieldHealth() ) + minDamage ) || !titan.IsInterruptable() )
+ {
+ // we took damage getting to the mortar location lets wait until we stop taking damage
+ waitthread MortarAIWaitToEngage( titan, MORTAR_TITAN_ENGAGE_DELAY )
+ continue
+ }
+
+ waitthread MortarTitanKneelToAttack( titan )
+ thread MortarTitanAttack( titan, generator )
+
+ wait minEngagementDuration // aways mortar the target for a while before potentially breaking out
+
+ // wait for interruption
+ waitthread WaitForInteruption( titan )
+
+ MortarTitanStopAttack_Internal( titan )
+
+ // lets wait until we stop taking damage before going back to attacking the generator
+ waitthread MortarAIWaitToEngage( titan, MORTAR_TITAN_REENGAGE_DELAY )
+ }
+}
+
+void function WaitForInteruption( entity titan )
+{
+ Assert( IsNewThread(), "Must be threaded off" )
+
+ titan.EndSignal( "OnSyncedMeleeVictim" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "InterruptMortarAttack" )
+
+ entity soul = titan.GetTitanSoul()
+ soul.EndSignal( "OnDestroy" )
+
+ float playerProximityDistSqr = pow( 256, 2 )
+ float healthBreakOff = ( titan.GetHealth() + soul.GetShieldHealth() ) * MORTAR_TITAN_ABORT_ATTACK_HEALTH_FRAC
+
+ while( true )
+ {
+ if ( IsEnemyWithinDist( titan, playerProximityDistSqr ) )
+ break
+ if ( ( titan.GetHealth() + soul.GetShieldHealth() ) < healthBreakOff )
+ break
+ wait 1
+ }
+}
+
+bool function IsEnemyWithinDist( entity titan, float dist )
+{
+ vector origin = titan.GetOrigin()
+ array<entity> players = GetPlayerArrayOfEnemies_Alive( titan.GetTeam() )
+
+ foreach( player in players )
+ {
+ if ( DistanceSqr( player.GetOrigin(), origin ) < dist )
+ return true
+ }
+
+ return false
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_nuke_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_nuke_titans.gnut
new file mode 100644
index 000000000..0d4b43c92
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_nuke_titans.gnut
@@ -0,0 +1,129 @@
+untyped
+
+global function NukeTitanThink
+
+global function AutoTitan_SelfDestruct
+
+const NUKE_TITAN_PLAYER_DETECT_RANGE = 500
+const NUKE_TITAN_RANGE_CHECK_SLEEP_SECS = 1.0
+
+void function AutoTitan_SelfDestruct( entity titan )
+{
+ if ( titan.ContextAction_IsBusy() )
+ titan.ContextAction_ClearBusy()
+
+ thread TitanEjectPlayer( titan )
+}
+
+void function NukeTitanThink( entity titan, entity generator )
+{
+ //Function assumes that given Titan is spawned as npc_titan_ogre_meteor_nuke. Changing the Titan's AISettings post-spawn
+ //disrupts the Titan's titanfall animations and can result in the Titan landing outside the level.
+ NPC_SetNuclearPayload( titan )
+ AddEntityCallback_OnPostDamaged( titan, AutoTitan_NuclearPayload_PostDamageCallback )
+
+ WaitTillHotDropComplete( titan )
+
+ thread NukeTitanSeekOutGenerator( titan, generator )
+}
+
+
+void function NukeTitanSeekOutGenerator( entity titan, entity generator )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "Doomed" )
+
+ WaitSignal( titan, "FD_ReachedHarvester", "OnFailedToPath" )
+
+ float goalRadius = 100
+ float checkRadiusSqr = 400 * 400
+
+ //array<vector> pos = NavMesh_RandomPositions( generator.GetOrigin(), HULL_TITAN, 5, 250, 350 )
+ array<vector> pos = NavMesh_GetNeighborPositions( generator.GetOrigin(), HULL_TITAN, 5 )
+ pos = ArrayClosestVector( pos, titan.GetOrigin() )
+
+ array<vector> validPos
+ foreach ( point in pos )
+ {
+ if ( DistanceSqr( generator.GetOrigin(), point ) <= checkRadiusSqr && NavMesh_IsPosReachableForAI( titan, point ) )
+ {
+ validPos.append( point )
+ //DebugDrawSphere( point, 32, 255, 0, 0, true, 60 )
+ }
+ }
+
+ int posLen = validPos.len()
+ while( posLen >= 1 )
+ {
+ titan.SetEnemy( generator )
+ thread AssaultOrigin( titan, validPos[0], goalRadius )
+ titan.AssaultSetFightRadius( goalRadius )
+
+ wait 0.5
+
+ if ( DistanceSqr( titan.GetOrigin(), generator.GetOrigin() ) > checkRadiusSqr )
+ continue
+
+ break
+ }
+
+ thread AutoTitan_SelfDestruct( titan )
+}
+
+// intercept damage to nuke titans in damage callback so we can nuke them before death 100% of the time
+void function AutoTitan_NuclearPayload_PostDamageCallback( entity titan, var damageInfo )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ entity titanOwner = titan.GetBossPlayer()
+ if ( IsValid( titanOwner ) )
+ {
+ Assert( titanOwner.IsPlayer() )
+ Assert( GetPlayerTitanInMap( titanOwner ) == titan )
+ return
+ }
+
+ int nuclearPayload = NPC_GetNuclearPayload( titan )
+ if ( nuclearPayload == 0 )
+ return
+
+ if ( !GetDoomedState( titan ) )
+ return
+
+ if ( titan.GetTitanSoul().IsEjecting() )
+ return
+
+ // Nuke eject as soon as the titan enters doom state.
+ if ( !( "doomedStateNukeTriggerHealth" in titan.s ) )
+ {
+ titan.s.doomedStateNukeTriggerHealth <- titan.GetMaxHealth()
+ }
+
+ if ( titan.GetHealth() > titan.s.doomedStateNukeTriggerHealth )
+ {
+ //printt( "titan health:", titan.GetHealth(), "health to nuke:", titan.s.doomedStateNukeTriggerHealth )
+ return
+ }
+
+ printt( "NUKE TITAN DOOMED TRIGGER HEALTH REACHED, NUKING! Health:", titan.s.doomedStateNukeTriggerHealth )
+
+ thread AutoTitan_SelfDestruct( titan )
+}
+
+function AutoTitan_CanDoRangeCheck( autoTitan )
+{
+ if ( !( "nextPlayerTitanRangeCheckTime" in autoTitan.s ) )
+ autoTitan.s.nextPlayerTitanRangeCheckTime <- -1
+
+ if ( Time() < autoTitan.s.nextPlayerTitanRangeCheckTime )
+ {
+ return false
+ }
+ else
+ {
+ autoTitan.s.nextPlayerTitanRangeCheckTime = Time() + NUKE_TITAN_RANGE_CHECK_SLEEP_SECS
+ return true
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_personal_shield.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_personal_shield.gnut
new file mode 100644
index 000000000..f1fbdb80f
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_personal_shield.gnut
@@ -0,0 +1,371 @@
+global function AiPersonalShield
+global function ActivatePersonalShield
+const FX_DRONE_SHIELD_WALL_HUMAN = $"P_drone_shield_wall_sm"
+const SHIELD_BREAK_FX = $"P_xo_armor_break_CP"
+const SHIELD_HEALTH = 620
+global const AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE = true
+const float PERSONAL_SHIELD_HEALTH_FRAC_DAMAGED = 0.5 // below what frac of total health will the personal shield owner want to chatter about shield damage?
+
+struct
+{
+ table<entity, entity> npcVortexSpheres
+} file
+
+
+void function AiPersonalShield()
+{
+ PrecacheParticleSystem( FX_DRONE_SHIELD_WALL_HUMAN )
+ PrecacheParticleSystem( SHIELD_BREAK_FX )
+ AddSyncedMeleeServerCallback( GetSyncedMeleeChooser( "human", "human" ), DisableShieldOnExecution )
+}
+
+void function DisableShieldOnExecution( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target )
+{
+ if ( !( target in file.npcVortexSpheres ) )
+ return
+
+ entity vortex = file.npcVortexSpheres[ target ]
+ vortex.Destroy()
+}
+
+void function ActivatePersonalShield( entity owner )
+{
+ owner.EndSignal( "OnDeath" )
+ for ( ;; )
+ {
+ waitthread ActivatePersonalShield_Recreate( owner )
+
+ // got stunned? make new shield after awhile
+ wait 15
+ }
+}
+
+void function ShieldProtectsOwnerFromMelee( entity ent, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsAlive( attacker ) )
+ return
+ if ( !attacker.IsPlayer() )
+ return
+ if ( !IsPilot( attacker ) )
+ return
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ if ( !IsValid( weapon ) )
+ weapon = attacker.GetActiveWeapon()
+ if ( !IsValid( weapon ) )
+ return
+ var weaponType = weapon.GetWeaponInfoFileKeyField( "weaponType" )
+ if ( weaponType != "melee" )
+ return
+
+ Assert( ent in file.npcVortexSpheres )
+ entity vortexSphere = file.npcVortexSpheres[ ent ]
+
+ 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 )
+
+ vector origin = vortexSphere.GetOrigin()
+ vector angles = vortexSphere.GetAngles()
+ vector forward = AnglesToForward( angles )
+ int team = vortexSphere.GetTeam()
+
+ if ( ProtectedFromShield( attacker, origin, height, radius, bullet_fov, dot, forward ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ StunPushBack( attacker, forward )
+ }
+}
+
+entity function ActivatePersonalShield_Recreate( entity owner )
+{
+ if ( !AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ AddEntityCallback_OnDamaged( owner, ShieldProtectsOwnerFromMelee )
+ //------------------------------
+ // Shield vars
+ //------------------------------
+ vector origin = owner.GetOrigin()
+ vector angles = owner.GetAngles() + Vector( 0, 0, 180 )
+
+ float shieldWallRadius = 45 // 90
+ asset shieldFx = FX_DRONE_SHIELD_WALL_HUMAN
+ float wallFOV = DRONE_SHIELD_WALL_FOV_HUMAN
+ float shieldWallHeight = 102
+
+ //------------------------------
+ // Vortex to block the actual bullets
+ //------------------------------
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+
+ Assert( !( owner in file.npcVortexSpheres ), owner + " already has a shield" )
+ file.npcVortexSpheres[ owner ] <- vortexSphere
+ 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
+
+ StatusEffect_AddEndless( vortexSphere, eStatusEffect.destroyed_by_emp, 1.0 )
+
+ vortexSphere.SetAngles( angles ) // viewvec?
+ vortexSphere.SetOrigin( origin + Vector( 0, 0, shieldWallRadius - 64 ) )
+ vortexSphere.SetMaxHealth( SHIELD_HEALTH )
+ vortexSphere.SetHealth( SHIELD_HEALTH )
+ SetTeam( vortexSphere, owner.GetTeam() )
+
+ thread PROTO_VortexSlowsPlayers_PersonalShield( owner, vortexSphere )
+
+ DispatchSpawn( vortexSphere )
+
+ EntFireByHandle( vortexSphere, "Enable", "", 0, null, null )
+
+ vortexSphere.SetTakeDamageType( DAMAGE_YES )
+ vortexSphere.ClearInvulnerable() // make particle wall invulnerable to weapon damage. It will still drain over time
+
+ //------------------------------------------
+ // Shield wall fx for visuals/health drain
+ //------------------------------------------
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ entity mover = CreateScriptMover()
+ mover.SetOrigin( owner.GetOrigin() )
+ vector moverAngles = owner.GetAngles()
+ mover.SetAngles( AnglesCompose( moverAngles, <0,0,180> ) )
+
+ int fxid = GetParticleSystemIndex( FX_DRONE_SHIELD_WALL_HUMAN )
+ entity shieldWallFX = StartParticleEffectOnEntity_ReturnEntity( mover, fxid, FX_PATTACH_ABSORIGIN_FOLLOW, 0 )
+ shieldWallFX.DisableHibernation()
+ EffectSetControlPointEntity( shieldWallFX, 0, mover )
+
+ //thread DrawArrowOnTag( mover )
+ vortexSphere.e.shieldWallFX = shieldWallFX
+ vector color = GetShieldTriLerpColor( 0.0 )
+
+ cpoint.SetOrigin( color )
+ EffectSetControlPointEntity( shieldWallFX, 1, cpoint )
+ SetVortexSphereShieldWallCPoint( vortexSphere, cpoint )
+
+ #if GRUNTCHATTER_ENABLED
+ // have to do this, vortex shield isn't an entity that works with AddEntityCallback_OnDamaged
+ thread PersonalShieldOwner_ReactsToDamage( owner, vortexSphere )
+ #endif
+
+ //-----------------------
+ // Attach shield to owner
+ //------------------------
+ vortexSphere.SetParent( mover )
+
+ vortexSphere.EndSignal( "OnDestroy" )
+ Assert( IsAlive( owner ) )
+ owner.EndSignal( "OnDeath" )
+ owner.EndSignal( "ArcStunned" )
+ mover.EndSignal( "OnDestroy" )
+ #if MP
+ shieldWallFX.EndSignal( "OnDestroy" )
+ #endif
+
+ OnThreadEnd(
+ function() : ( owner, mover, vortexSphere )
+ {
+ delete file.npcVortexSpheres[ owner ]
+ if ( IsValid( owner ) )
+ {
+ owner.kv.defenseActive = false
+ if ( !AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ RemoveEntityCallback_OnDamaged( owner, ShieldProtectsOwnerFromMelee )
+ }
+
+ StopShieldWallFX( vortexSphere )
+
+ if ( IsValid( vortexSphere ) )
+ vortexSphere.Destroy()
+
+ if ( IsValid( mover ) )
+ {
+ //PlayFX( SHIELD_BREAK_FX, mover.GetOrigin(), mover.GetAngles() )
+ mover.Destroy()
+ }
+ }
+ )
+
+ owner.kv.defenseActive = true
+
+ for ( ;; )
+ {
+ Assert( IsAlive( owner ) )
+ UpdateShieldPosition( mover, owner )
+
+ #if MP
+ if ( IsCloaked( owner ) )
+ EntFireByHandle( shieldWallFX, "Stop", "", 0, null, null )
+ else
+ EntFireByHandle( shieldWallFX, "Start", "", 0, null, null )
+ #endif
+ }
+}
+
+#if GRUNTCHATTER_ENABLED
+void function PersonalShieldOwner_ReactsToDamage( entity owner, entity vortexSphere )
+{
+ EndSignal( owner, "OnDeath" )
+ EndSignal( vortexSphere, "OnDestroy" )
+
+ float alertHealth = vortexSphere.GetMaxHealth() * PERSONAL_SHIELD_HEALTH_FRAC_DAMAGED
+
+ while ( vortexSphere.GetHealth() >= alertHealth )
+ wait 0.25
+
+ GruntChatter_TryPersonalShieldDamaged( owner ) //Commenting out to unblock tree. See bug 186062
+}
+#endif
+
+float function GetYawForEnemyOrLKP( entity owner )
+{
+ entity enemy = owner.GetEnemy()
+ if ( !IsValid( enemy ) )
+ return owner.GetAngles().y
+
+ vector ornull lkp = owner.LastKnownPosition( enemy )
+ if ( lkp == null )
+ return owner.GetAngles().y
+
+ expect vector( lkp )
+ vector dif = lkp - owner.GetOrigin()
+ return VectorToAngles( dif ).y
+}
+
+void function UpdateShieldPosition( entity mover, entity owner )
+{
+ mover.NonPhysicsMoveTo( owner.GetOrigin(), 0.1, 0.0, 0.0 )
+ vector angles = owner.EyeAngles()
+ float yaw = angles.y
+ yaw %= 360
+ mover.NonPhysicsRotateTo( <0,yaw,180>, 1.35, 0, 0 )
+
+// float yaw = GetYawForEnemyOrLKP( owner )
+// float boost = sin( Time() * 1.5 ) * 65
+// yaw += boost
+// yaw %= 360
+// mover.NonPhysicsRotateTo( <0,yaw,0>, 0.95, 0, 0 )
+
+ WaitFrame()
+}
+
+void function PROTO_VortexSlowsPlayers_PersonalShield( entity owner, entity vortexSphere )
+{
+ owner.EndSignal( "OnDeath" )
+ vortexSphere.EndSignal( "OnDestroy" )
+
+ 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 = vortexSphere.GetTeam()
+
+ foreach ( player in GetPlayerArray() )
+ {
+ if ( !IsAlive( player ) )
+ continue
+ if ( player.GetTeam() == team )
+ continue
+ if ( VortexStunCheck_PersonalShield( player, origin, height, radius, bullet_fov, dot, forward ) )
+ {
+ player.p.lastDroneShieldStunPushTime = Time()
+
+ if ( AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ {
+ Explosion_DamageDefSimple( damagedef_shield_captain_arc_shield, player.GetOrigin(),owner, owner, player.GetOrigin() )
+ }
+ }
+ }
+ WaitFrame()
+ }
+}
+
+bool function ProtectedFromShield( entity player, vector origin, float height, float radius, float bullet_fov, float dotLimit, vector forward )
+{
+ vector playerOrg = player.GetOrigin()
+ vector dif = Normalize( playerOrg - origin )
+
+ float dot = DotProduct2D( dif, forward )
+ return dot >= dotLimit
+}
+
+bool function VortexStunCheck_PersonalShield( entity player, vector origin, float height, float radius, float bullet_fov, float dot, vector forward )
+{
+ if ( !IsPilot( player ) )
+ return false
+
+ if ( player.IsGodMode() )
+ return false
+
+ if ( AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ {
+ if ( Time() - player.p.lastDroneShieldStunPushTime < 1.00 )
+ return false
+ }
+ else
+ {
+ if ( Time() - player.p.lastDroneShieldStunPushTime < 1.75 )
+ return false
+ }
+
+ vector playerOrg = player.GetOrigin()
+ float dist2d = Distance2D( playerOrg, origin )
+
+ if ( dist2d > radius + 5 )
+ return false
+ if ( dist2d < radius - 15 )
+ return false
+
+ float heightOffset = fabs( playerOrg.z - origin.z )
+
+ if ( heightOffset < 0 || heightOffset > height )
+ return false
+
+ if ( !ProtectedFromShield( player, origin, height, radius, bullet_fov, dot, forward ) )
+ return false
+
+ if ( AI_PERSONAL_SHIELD_PAIN_SHIELD_STYLE )
+ {
+ 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 )
+ EmitSoundOnEntityOnlyToPlayer( player, player, "flesh_electrical_damage_1p" )
+ }
+ else
+ {
+ StunPushBack( player, forward )
+ }
+
+ return true
+}
+
+void function StunPushBack( entity player, vector forward )
+{
+ 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
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "flesh_electrical_damage_1p" )
+ player.SetVelocity( velocity )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_pilots.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_pilots.gnut
new file mode 100644
index 000000000..3c2e36ce0
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_pilots.gnut
@@ -0,0 +1,808 @@
+untyped
+
+global const NPC_TITAN_PILOT_PROTOTYPE = 0
+global function AiPilots_Init
+
+global function CaptainThink
+
+
+#if NPC_TITAN_PILOT_PROTOTYPE
+global function NpcPilotCallTitanThink
+global function NpcPilotStopCallTitanThink
+global function NpcPilotCallsInAndEmbarksTitan
+global function NpcPilotRunsToAndEmbarksFallingTitan
+global function NpcPilotCallsInTitan
+global function NpcPilotRunsToEmbarkTitan
+global function NpcPilotEmbarksTitan
+global function NpcPilotDisembarksTitan
+global function NpcPilotBecomesTitan
+global function NpcTitanBecomesPilot
+global function TitanHasNpcPilot
+global function NpcPilotGetPetTitan
+global function NpcPilotSetPetTitan
+#endif
+
+global function NpcSetNextTitanRespawnAvailable
+global function NpcResetNextTitanRespawnAvailable
+
+global function AddCallback_OnNpcTitanBecomesPilot
+global function AddCallback_OnNpcPilotBecomesTitan
+
+global struct NPCPilotStruct
+{
+ bool isValid = false
+
+ int team
+ int spawnflags
+ float accuracy
+ float proficieny
+ float health
+ float physDamageScale
+ string weapon
+ string squadName
+
+ asset modelAsset
+ string title
+
+ bool isInvulnerable
+}
+
+const NPC_NEXT_TITANTIME_RESET = -1
+const NPC_NEXT_TITANTIME_MIN = 45
+const NPC_NEXT_TITANTIME_MAX = 60
+const NPC_NEXT_TITANTIME_INTERUPT = 15
+
+function AiPilots_Init()
+{
+ RegisterSignal( "grenade_throw" )
+ RegisterSignal( "NpcPilotBecomesTitan" )
+ RegisterSignal( "NpcTitanBecomesPilot" )
+ RegisterSignal( "StopCallTitanThink" )
+ RegisterSignal( "NpcTitanRespawnAvailableUpdated" )
+
+ level.onNpcPilotBecomesTitanCallbacks <- []
+ level.onNpcTitanBecomesPilotCallbacks <- []
+
+}
+
+function ScriptCallback_OnNpcPilotBecomesTitan( pilot, titan )
+{
+ local result = { pilot = pilot, titan = titan }
+ Signal( pilot, "NpcPilotBecomesTitan", result )
+ Signal( titan, "NpcPilotBecomesTitan", result )
+
+ foreach ( callbackFunc in level.onNpcPilotBecomesTitanCallbacks )
+ {
+ callbackFunc( pilot, titan )
+ }
+}
+
+function ScriptCallback_OnNpcTitanBecomesPilot( pilot, titan )
+{
+ local result = { pilot = pilot, titan = titan }
+ Signal( pilot, "NpcTitanBecomesPilot", result )
+ Signal( titan, "NpcTitanBecomesPilot", result )
+
+ foreach ( callbackFunc in level.onNpcTitanBecomesPilotCallbacks )
+ {
+ callbackFunc( pilot, titan )
+ }
+}
+
+function AddCallback_OnNpcPilotBecomesTitan( callbackFunc )
+{
+ Assert( "onNpcPilotBecomesTitanCallbacks" in level )
+ AssertParameters( callbackFunc, 2, "pilotNPC, titanNPC" )
+
+ level.onNpcPilotBecomesTitanCallbacks.append( callbackFunc )
+}
+
+function AddCallback_OnNpcTitanBecomesPilot( callbackFunc )
+{
+ Assert( "onNpcTitanBecomesPilotCallbacks" in level )
+ AssertParameters( callbackFunc, 2, "pilotNPC, titanNPC" )
+
+ level.onNpcTitanBecomesPilotCallbacks.append( callbackFunc )
+}
+
+function NpcSetNextTitanRespawnAvailable( npc, time )
+{
+ Assert( "nextTitanRespawnAvailable" in npc.s )
+ npc.s.nextTitanRespawnAvailable = time
+ npc.Signal( "NpcTitanRespawnAvailableUpdated" )
+}
+
+function NpcResetNextTitanRespawnAvailable( npc )
+{
+ Assert( "nextTitanRespawnAvailable" in npc.s )
+ npc.s.nextTitanRespawnAvailable = NPC_NEXT_TITANTIME_RESET
+ npc.Signal( "NpcTitanRespawnAvailableUpdated" )
+}
+
+function NpcPilotStopCallTitanThink( pilot )
+{
+ pilot.Signal( "StopCallTitanThink" )
+}
+
+/************************************************************************************************\
+
+######## #### ## ####### ######## ######## ## ## #### ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ### ## ## ##
+## ## ## ## ## ## ## ## ## ## ## #### ## ## ##
+######## ## ## ## ## ## ## ######### ## ## ## ## #####
+## ## ## ## ## ## ## ## ## ## ## #### ## ##
+## ## ## ## ## ## ## ## ## ## ## ### ## ##
+## #### ######## ####### ## ## ## ## #### ## ## ## ##
+
+\************************************************************************************************/
+function CaptainThink( entity npc )
+{
+ npc.EndSignal( "OnDestroy" )
+ npc.EndSignal( "OnDeath" )
+
+ Assert( !( "nextTitanRespawnAvailable" in npc.s ) )
+ Assert( !( "petTitan" in npc.s ) )
+
+ npc.s.petTitan <- null
+ npc.s.nextTitanRespawnAvailable <- null
+
+ //wait for in combat...
+ WaitForNpcInCombat( npc )
+
+ //... before we call in a titan
+ if ( npc.s.nextTitanRespawnAvailable == null )
+ npc.s.nextTitanRespawnAvailable = Time() + RandomFloatRange( 2, 10 )
+
+ WaitEndFrame() //wait a frame for things like petTitan and nextTitanRespawnAvailable to have a chance to be set from custom scripts
+ #if NPC_TITAN_PILOT_PROTOTYPE
+ thread NpcPilotCallTitanThink( npc )
+ #endif
+}
+
+#if NPC_TITAN_PILOT_PROTOTYPE
+
+function NpcPilotCallTitanThink( entity pilot )
+{
+ Assert( pilot.IsNPC() )
+ Assert( IsAlive( pilot ) )
+ Assert ( !pilot.IsTitan() )
+
+ pilot.EndSignal( "OnDestroy" )
+ pilot.EndSignal( "OnDeath" )
+ pilot.Signal( "StopCallTitanThink" )
+ pilot.EndSignal( "StopCallTitanThink" )
+
+
+ string title = pilot.GetTitle() + "'s Titan"
+ local count = 1 //1 titan call in at a time
+
+ while ( true ) //this loop usually only happens once, unless the titan called in is destroyed before the living pilot can get to it
+ {
+ entity titan = NpcPilotGetPetTitan( pilot )
+ if ( !IsAlive( titan ) )
+ {
+ //wait for ready titan
+ waitthread __WaitforTitanCallinReady( pilot )
+
+ //ready to call in - look for a good spot
+ SpawnPointFP spawnPoint
+ while ( true )
+ {
+ wait ( RandomFloatRange( 1, 2 ) )
+
+ //dont do stuff when animating on a parent
+ if ( pilot.GetParent() )
+ continue
+
+ //Don't deploy if too close to an enemy
+ if ( HasEnemyWithinDist( pilot, 300.0 ) )
+ continue
+
+ // DO the opposite - only deploy if has an enemy within this distance
+ // if ( !HasEnemyWithinDist( pilot, 2000.0 ) )
+ // continue
+
+ //don't do stuff if you dont have a spawnPoint
+ spawnPoint = FindSpawnPointForNpcCallin( pilot, TITAN_MEDIUM_AJAX_MODEL, HOTDROP_TURBO_ANIM )
+ if ( !spawnPoint.valid )
+ continue
+
+ break
+ }
+
+ //call in a titan, run to it, and embark
+ //in SP by default, the friendlys do NOT do the beacon tell
+ titan = NpcPilotCallsInAndEmbarksTitan( pilot, spawnPoint.origin, spawnPoint.angles )
+ titan.SetTitle( title )
+ }
+ else
+ {
+ Assert( IsAlive( titan ) )
+
+ if ( HasEnemyRodeo( titan ) )
+ {
+ while ( HasEnemyRodeo( titan ) )
+ {
+ WaitSignal( titan.GetTitanSoul(), "RodeoRiderChanged", "OnDestroy" )
+ }
+
+ wait 4 //don't pop back in immediately
+ }
+
+ if ( !IsAlive( titan ) )
+ continue //the titan didn't make it, lets loop back up and try again
+
+ if ( titan.GetTitanSoul().IsDoomed() )
+ {
+ titan.WaitSignal( "OnDestroy" )
+ continue //the titan didn't make it, lets loop back up and try again
+ }
+
+ //start running to titan as it kneels
+ thread NpcPilotRunsToEmbarkTitan( pilot, titan )
+ thread __TitanKneelsForPilot( pilot, titan )
+ wait 2.0 //wait for titan to be in position
+
+ if ( !IsAlive( titan ) )
+ continue //the titan didn't make it, lets loop back up and try again
+
+ //run to the titan
+ waitthread NpcPilotRunsToEmbarkTitan( pilot, titan )
+
+ if ( !IsAlive( titan ) )
+ continue //the titan didn't make it, lets loop back up and try again
+
+ //embark titan
+ thread NpcPilotEmbarksTitan( pilot, titan )
+ }
+
+ local result = WaitSignal( titan, "NpcPilotBecomesTitan", "OnDeath", "OnDestroy" )
+ if ( result.signal != "NpcPilotBecomesTitan" )
+ continue //the titan didn't make it, lets loop back up and try again
+ }
+}
+
+/************************************************************************************************\
+
+ ###### ### ## ## #### ## ## ######## #### ######## ### ## ##
+## ## ## ## ## ## ## ### ## ## ## ## ## ## ### ##
+## ## ## ## ## ## #### ## ## ## ## ## ## #### ##
+## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
+## ######### ## ## ## ## #### ## ## ## ######### ## ####
+## ## ## ## ## ## ## ## ### ## ## ## ## ## ## ###
+ ###### ## ## ######## ######## #### ## ## ## #### ## ## ## ## ##
+
+\************************************************************************************************/
+
+
+entity function NpcPilotCallsInAndEmbarksTitan( entity pilot, vector origin, vector angles )
+{
+ entity titan = NpcPilotCallsInTitan( pilot, origin, angles )
+ thread NpcPilotRunsToAndEmbarksFallingTitan( pilot, titan )
+
+ return titan
+}
+
+function NpcPilotRunsToAndEmbarksFallingTitan( entity pilot, entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+
+ //wait for it to land
+ waitthread WaitTillHotDropComplete( titan )
+ ShowName( titan )
+
+ if ( !IsAlive( titan ) )
+ return
+ titan.EndSignal( "OnDeath" )
+
+ //titan is alive on land so clean it up on thread end
+ OnThreadEnd(
+ function () : ( titan )
+ {
+ if ( !IsAlive( titan ) )
+ return
+
+ SetStanceStand( titan.GetTitanSoul() )
+
+ //the pilot never made it to embark - lets stand our titan up so he can fight
+ if ( !TitanHasNpcPilot( titan ) )
+ {
+ thread PlayAnimGravity( titan, "at_hotdrop_quickstand" )
+ HideName( titan )
+ }
+ }
+ )
+
+ //if the pilot has died, early out
+ if ( !IsAlive( pilot ) )
+ return
+
+ pilot.EndSignal( "OnDeath" )
+
+ //run to the titan
+ waitthread NpcPilotRunsToEmbarkTitan( pilot, titan )
+
+ //embark titan
+ waitthread NpcPilotEmbarksTitan( pilot, titan )
+}
+
+entity function NpcPilotCallsInTitan( entity pilot, vector origin, vector angles )
+{
+ Assert( !pilot.IsTitan() )
+ Assert( IsAlive( pilot ) )
+ Assert( !NpcPilotGetPetTitan( pilot ) )
+
+ //reset the next titan callin timer
+ NpcResetNextTitanRespawnAvailable( pilot )
+
+ //spawn a titan
+ array<string> settingsArray = GetAllowedTitanAISettings()
+
+ string titanSettings = settingsArray.getrandom()
+ entity titan = CreateNPC( "npc_titan", pilot.GetTeam(), origin, angles )
+ SetSpawnOption_AISettings( titan, titanSettings )
+ DispatchSpawn( titan )
+
+ NpcPilotSetPetTitan( pilot, titan )
+
+ //call it in
+ thread NPCTitanHotdrops( titan, false, "at_hotdrop_drop_2knee_turbo_upgraded" )
+ thread __TitanKneelOrStandAfterDropin( titan, pilot )
+
+ //get the titan ready to be embarked
+ SetStanceKneel( titan.GetTitanSoul() )
+ titan.SetTitle( pilot.GetTitle() + "'s Titan" )
+ UpdateEnemyMemoryFromTeammates( titan )
+
+ return titan
+}
+
+void function __TitanKneelOrStandAfterDropin( entity titan, entity pilot )
+{
+ Assert( IsAlive( titan ) )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ titan.WaitSignal( "TitanHotDropComplete" )
+
+ if ( IsAlive( pilot ) )
+ thread PlayAnimGravity( titan, "at_MP_embark_idle" )
+ //else the titan will automatically stand up
+}
+
+//HACK -> this behavior should be completely in code
+void function NpcPilotRunsToEmbarkTitan( entity pilot, entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ pilot.EndSignal( "OnDeath" )
+ pilot.EndSignal( "OnDestroy" )
+
+ pilot.SetNoTarget( true )
+ pilot.Anim_Stop()
+ pilot.DisableNPCMoveFlag( NPCMF_INDOOR_ACTIVITY_OVERRIDE )
+ pilot.EnableNPCMoveFlag( NPCMF_IGNORE_CLUSTER_DANGER_TIME | NPCMF_PREFER_SPRINT )
+ pilot.DisableArrivalOnce( true )
+ bool canMoveAndShoot = pilot.GetCapabilityFlag( bits_CAP_MOVE_SHOOT )
+ pilot.SetCapabilityFlag( bits_CAP_MOVE_SHOOT, false )
+
+ OnThreadEnd(
+ function () : ( pilot, canMoveAndShoot )
+ {
+ if ( !IsAlive( pilot ) )
+ return
+
+ pilot.SetNoTarget( false )
+ pilot.EnableNPCMoveFlag( NPCMF_INDOOR_ACTIVITY_OVERRIDE )
+ pilot.DisableNPCMoveFlag( NPCMF_IGNORE_CLUSTER_DANGER_TIME | NPCMF_PREFER_SPRINT )
+ pilot.SetCapabilityFlag( bits_CAP_MOVE_SHOOT, canMoveAndShoot )
+ }
+ )
+
+ local titanSubClass = GetSoulTitanSubClass( titan.GetTitanSoul() )
+ local embarkSet = FindBestEmbarkForNpcAnim( pilot, titan )
+ string pilotAnim = GetAnimFromAlias( titanSubClass, embarkSet.animSet.thirdPersonKneelingAlias )
+
+ pilot.ClearAllEnemyMemory()
+ waitthread RunToAnimStartForced_Deprecated( pilot, pilotAnim, titan, "hijack" )
+}
+
+/************************************************************************************************\
+
+ ###### ## ## #### ######## ###### ## ##
+## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ##
+ ###### ## ## ## ## ## ## #########
+ ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ##
+ ###### ### ### #### ## ###### ## ##
+
+\************************************************************************************************/
+function NpcPilotEmbarksTitan( entity pilot, entity titan )
+{
+ Assert( IsAlive( pilot ) )
+ Assert( IsAlive( titan ) )
+ Assert( !pilot.IsTitan() )
+ Assert( titan.IsTitan() )
+
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDeath" )
+
+ OnThreadEnd(
+ function () : ( titan, pilot )
+ {
+ if ( IsAlive( titan ) )
+ {
+ if ( titan.ContextAction_IsBusy() )
+ titan.ContextAction_ClearBusy()
+ titan.ClearInvulnerable()
+
+ Assert( !IsAlive( pilot ) )
+ }
+ }
+ )
+
+ local isInvulnerable = pilot.IsInvulnerable()
+ pilot.SetInvulnerable()
+ titan.SetInvulnerable()
+
+ local titanSubClass = GetSoulTitanSubClass( titan.GetTitanSoul() )
+ local embarkSet = FindBestEmbark( pilot, titan )
+
+ while ( embarkSet == null )
+ {
+ wait 1.0
+ embarkSet = FindBestEmbark( pilot, titan )
+ }
+
+ local pilotAnim = GetAnimFromAlias( titanSubClass, embarkSet.animSet.thirdPersonKneelingAlias )
+ local titanAnim = embarkSet.animSet.titanKneelingAnim
+
+ if ( !titan.ContextAction_IsBusy() ) //might be set from kneeling
+ titan.ContextAction_SetBusy()
+ pilot.ContextAction_SetBusy()
+
+ if ( IsCloaked( pilot ) )
+ pilot.SetCloakDuration( 0, 0, 1.5 )
+
+ //pilot.SetParent( titan, "hijack", false, 0.5 ) //the time is just in case their not exactly at the right starting position
+ EmitSoundOnEntity( titan, embarkSet.audioSet.thirdPersonKneelingAudioAlias )
+ thread PlayAnim( pilot, pilotAnim, titan, "hijack" )
+ waitthread PlayAnim( titan, titanAnim )
+
+ if ( !isInvulnerable )
+ pilot.ClearInvulnerable()
+
+ NpcPilotBecomesTitan( pilot, titan )
+}
+
+entity function NpcPilotDisembarksTitan( entity titan )
+{
+ Assert( titan.IsTitan() )
+ Assert( TitanHasNpcPilot( titan ) )
+
+ entity pilot = NpcTitanBecomesPilot( titan )
+ Assert( !pilot.IsTitan() )
+
+ NpcPilotSetPetTitan( pilot, titan )
+
+ thread __NpcPilotDisembarksTitan( pilot, titan )
+
+ return pilot
+}
+
+function __NpcPilotDisembarksTitan( pilot, titan )
+{
+ expect entity( pilot )
+ expect entity( titan )
+
+ titan.ContextAction_SetBusy()
+ pilot.ContextAction_SetBusy()
+
+ if ( pilot.GetTitle() != "" )
+ {
+ titan.SetTitle( pilot.GetTitle() + "'s Titan" )
+ }
+
+ local isInvulnerable = pilot.IsInvulnerable()
+ pilot.SetInvulnerable()
+ titan.SetInvulnerable()
+
+ local pilot3pAnim, pilot3pAudio, titanDisembarkAnim
+ local titanSubClass = GetSoulTitanSubClass( titan.GetTitanSoul() )
+ local standing = titan.GetTitanSoul().GetStance() >= STANCE_STANDING // STANCE_STANDING = 2, STANCE_STAND = 3
+
+ if ( standing )
+ {
+ titanDisembarkAnim = "at_dismount_stand"
+ pilot3pAnim = "pt_dismount_" + titanSubClass + "_stand"
+ pilot3pAudio = titanSubClass + "_Disembark_Standing_3P"
+ }
+ else
+ {
+ titanDisembarkAnim = "at_dismount_crouch"
+ pilot3pAnim = "pt_dismount_" + titanSubClass + "_crouch"
+ pilot3pAudio = titanSubClass + "_Disembark_Kneeling_3P"
+ }
+
+// pilot.SetParent( titan, "hijack" )
+ EmitSoundOnEntity( titan, pilot3pAudio )
+ thread PlayAnim( titan, titanDisembarkAnim )
+ waitthread PlayAnim( pilot, pilot3pAnim, titan, "hijack" )
+
+ //pilot.ClearParent()
+ titan.ContextAction_ClearBusy()
+ pilot.ContextAction_ClearBusy()
+ if ( !isInvulnerable )
+ pilot.ClearInvulnerable()
+ titan.ClearInvulnerable()
+
+ if ( !standing )
+ SetStanceKneel( titan.GetTitanSoul() )
+}
+
+void function NpcPilotBecomesTitan( entity pilot, entity titan )
+{
+ Assert( IsAlive( pilot ) )
+ Assert( IsAlive( titan ) )
+ Assert( IsGrunt( pilot ) || IsPilotElite( pilot ) )
+ Assert( titan.IsTitan() )
+
+ entity titanSoul = titan.GetTitanSoul()
+
+ titanSoul.soul.seatedNpcPilot.isValid = true
+
+ titanSoul.soul.seatedNpcPilot.team = pilot.GetTeam()
+ titanSoul.soul.seatedNpcPilot.spawnflags = expect int( pilot.kv.spawnflags )
+ titanSoul.soul.seatedNpcPilot.accuracy = expect float( pilot.kv.AccuracyMultiplier )
+ titanSoul.soul.seatedNpcPilot.proficieny = expect float( pilot.kv.WeaponProficiency )
+ titanSoul.soul.seatedNpcPilot.health = expect float( pilot.kv.max_health )
+ titanSoul.soul.seatedNpcPilot.physDamageScale = expect float( pilot.kv.physdamagescale )
+ titanSoul.soul.seatedNpcPilot.weapon = pilot.GetMainWeapons()[0].GetWeaponClassName()
+ titanSoul.soul.seatedNpcPilot.squadName = expect string( pilot.kv.squadname )
+
+ titanSoul.soul.seatedNpcPilot.modelAsset = pilot.GetModelName()
+ titanSoul.soul.seatedNpcPilot.title = pilot.GetTitle()
+
+ titanSoul.soul.seatedNpcPilot.isInvulnerable = pilot.IsInvulnerable()
+
+ titan.SetTitle( titanSoul.soul.seatedNpcPilot.title )
+
+ thread __TitanPilotRodeoCounter( titan )
+
+ ScriptCallback_OnNpcPilotBecomesTitan( pilot, titan )
+
+ pilot.Destroy()
+}
+
+entity function NpcTitanBecomesPilot( entity titan )
+{
+ Assert( IsValid( titan ) )
+ Assert( titan.IsTitan() )
+
+ entity titanSoul = titan.GetTitanSoul()
+ titanSoul.soul.seatedNpcPilot.isValid = false
+
+ string weapon = titanSoul.soul.seatedNpcPilot.weapon
+ string squadName = titanSoul.soul.seatedNpcPilot.squadName
+ asset model = titanSoul.soul.seatedNpcPilot.modelAsset
+ string title = titanSoul.soul.seatedNpcPilot.title
+ int team = titanSoul.soul.seatedNpcPilot.team
+ vector origin = titan.GetOrigin()
+ vector angles = titan.GetAngles()
+ entity pilot = CreateElitePilot( team, origin, angles )
+
+ SetSpawnOption_Weapon( pilot, weapon )
+ SetSpawnOption_SquadName( pilot, squadName )
+ pilot.SetValueForModelKey( model )
+ DispatchSpawn( pilot )
+ pilot.SetModel( model ) // this is a hack, trying to avoid having a model spawn option because its easy to abuse
+
+ NpcPilotSetPetTitan( pilot, titan )
+ NpcResetNextTitanRespawnAvailable( pilot )
+
+ pilot.kv.spawnflags = titanSoul.soul.seatedNpcPilot.spawnflags
+ pilot.kv.AccuracyMultiplier = titanSoul.soul.seatedNpcPilot.accuracy
+ pilot.kv.WeaponProficiency = titanSoul.soul.seatedNpcPilot.proficieny
+ pilot.kv.health = titanSoul.soul.seatedNpcPilot.health
+ pilot.kv.max_health = titanSoul.soul.seatedNpcPilot.health
+ pilot.kv.physDamageScale = titanSoul.soul.seatedNpcPilot.physDamageScale
+
+ if ( titanSoul.soul.seatedNpcPilot.isInvulnerable )
+ pilot.SetInvulnerable()
+
+ titan.SetOwner( pilot )
+ NPCFollowsNPC( titan, pilot )
+
+ UpdateEnemyMemoryFromTeammates( pilot )
+ thread __TitanStanceThink( pilot, titan )
+
+ ScriptCallback_OnNpcTitanBecomesPilot( pilot, titan )
+
+ return pilot
+}
+
+bool function TitanHasNpcPilot( entity titan )
+{
+ Assert( titan.IsTitan() )
+
+ entity titanSoul = titan.GetTitanSoul()
+ if ( !IsValid( titanSoul ) )
+ return false
+
+ if ( !titanSoul.soul.seatedNpcPilot.isValid )
+ return false
+
+ return true
+}
+
+entity function NpcPilotGetPetTitan( entity pilot )
+{
+ Assert( !pilot.IsTitan() )
+ Assert( "petTitan" in pilot.s )
+
+ if ( !IsAlive( expect entity( pilot.s.petTitan ) ) )
+ return null
+
+ Assert( pilot.s.petTitan.IsTitan() )
+ return expect entity( pilot.s.petTitan )
+}
+
+void function NpcPilotSetPetTitan( entity pilot, entity titan )
+{
+ Assert( !pilot.IsTitan() )
+ Assert( titan.IsTitan() )
+ Assert( "petTitan" in pilot.s )
+
+ pilot.s.petTitan = titan
+ pilot.Signal( "PetTitanUpdated" )
+}
+#endif // NPC_TITAN_PILOT_PROTOTYPE
+
+function __TitanStanceThink( entity pilot, entity titan )
+{
+ if ( !IsAlive( titan ) )
+ return
+
+ if ( titan.GetTitanSoul().IsDoomed() )
+ return
+
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "NpcPilotBecomesTitan" )
+
+ WaittillAnimDone( titan ) //wait for disembark anim
+
+ // kneel in certain circumstances
+ while ( IsAlive( pilot ) )
+ {
+ if ( !ChangedStance( titan ) )
+ waitthread TitanWaitsToChangeStance_or_PilotDeath( pilot, titan )
+ }
+
+ if ( titan.GetTitanSoul().GetStance() < STANCE_STANDING )
+ {
+ while ( !TitanCanStand( titan ) )
+ wait 2
+
+ TitanStandUp( titan )
+ }
+}
+
+function TitanWaitsToChangeStance_or_PilotDeath( pilot, titan )
+{
+ pilot.EndSignal( "OnDeath" )
+ pilot.EndSignal( "OnDestroy" )
+
+ TitanWaitsToChangeStance( titan )
+}
+
+/************************************************************************************************\
+
+######## ####### ####### ## ######
+ ## ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ######
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ## ##
+ ## ####### ####### ######## ######
+
+\************************************************************************************************/
+
+function __WaitforTitanCallinReady( entity pilot )
+{
+ pilot.EndSignal( "OnDeath" )
+ pilot.EndSignal( "OnDestroy" )
+
+ //HACK TODO: handle eTitanAvailability.Default vs custom and none, AND ALSO make a way to kill this thread
+
+ while ( true )
+ {
+ if ( pilot.s.nextTitanRespawnAvailable == NPC_NEXT_TITANTIME_RESET )
+ pilot.s.nextTitanRespawnAvailable = Time() + RandomFloatRange( NPC_NEXT_TITANTIME_MIN, NPC_NEXT_TITANTIME_MAX ) //this is just a random number - maybe in the future it will be based on the npc's kills...maybe also on the players if it's a slot
+
+ if ( pilot.s.nextTitanRespawnAvailable <= Time() )
+ break
+
+ float delay = max( pilot.s.nextTitanRespawnAvailable - Time(), 0.1 ) //make sure min delay of 0.1 to account for floating point error
+
+ thread SetSignalDelayed( pilot, "NpcTitanRespawnAvailableUpdated", delay )
+ pilot.WaitSignal( "NpcTitanRespawnAvailableUpdated" )
+
+ //keep looping backup just in case this value changes outside this function, we get an update
+ continue
+ }
+
+ Assert( Time() >= pilot.s.nextTitanRespawnAvailable )
+ Assert( pilot.s.nextTitanRespawnAvailable != NPC_NEXT_TITANTIME_RESET )
+}
+
+function __TitanKneelsForPilot( pilot, titan )
+{
+ expect entity( pilot )
+ expect entity( titan )
+
+ pilot.EndSignal( "OnDeath" )
+ pilot.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function () : ( pilot, titan )
+ {
+ if ( !IsAlive( titan ) )
+ return
+
+ SetStanceStand( titan.GetTitanSoul() )
+
+ //the pilot never made it to embark - lets stand our titan up so he can fight
+ if ( !IsAlive( pilot ) )
+ {
+ thread PlayAnimGravity( titan, "at_hotdrop_quickstand" )
+ HideName( titan )
+ titan.ContextAction_ClearBusy()
+ }
+ }
+ )
+
+ if ( !titan.ContextAction_IsBusy() ) //might be set from kneeling
+ titan.ContextAction_SetBusy()
+ SetStanceKneel( titan.GetTitanSoul() )
+
+ waitthread PlayAnimGravity( titan, "at_MP_stand2knee_straight" )
+ waitthread PlayAnim( titan, "at_MP_embark_idle" )
+}
+
+function HasEnemyRodeo( titan )
+{
+ expect entity( titan )
+
+ if ( !IsAlive( titan ) )
+ return false
+
+ if ( IsValid( GetEnemyRodeoPilot( titan ) ) )
+ return true
+
+ return false
+}
+
+function __TitanPilotRodeoCounter( entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ while ( true )
+ {
+ while ( !HasEnemyRodeo( titan ) )
+ titan.GetTitanSoul().WaitSignal( "RodeoRiderChanged" )
+
+ wait RandomFloatRange( 3, 6 ) //give some time for debounce in case the rider jumps right off
+ if ( !HasEnemyRodeo( titan ) )
+ continue
+
+ #if NPC_TITAN_PILOT_PROTOTYPE
+ thread NpcPilotDisembarksTitan( titan )
+ return
+ #endif
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_sniper_titans.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_sniper_titans.gnut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_sniper_titans.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut
new file mode 100644
index 000000000..9717c76d9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut
@@ -0,0 +1,787 @@
+untyped
+
+global const RPG_USE_ALWAYS = 2
+
+global const STANDARDGOALRADIUS = 100
+
+global function AiSoldiers_Init
+
+global function MakeSquadName
+global function GetPlayerSpectreSquadName
+global function disable_npcs
+global function disable_new_npcs
+global function Disable_IMC
+global function Disable_MILITIA
+
+global function CommonMinionInit
+global function DisableMinionUsesHeavyWeapons
+global function SetupMinionForRPGs
+global function IsNPCSpawningEnabled
+global function EnableAutoPopulate
+global function DisableAutoPopulate
+global function OnEnemyChanged_MinionSwitchToHeavyArmorWeapon
+global function OnEnemyChanged_MinionUpdateAimSettingsForEnemy
+global function OnEnemyChanged_TryHeavyArmorWeapon
+global function ResetNPCs
+global function IsValidRocketTarget
+global function GetMilitiaTitle
+
+global function AssaultOrigin
+global function SquadAssaultOrigin
+
+global function ClientCommand_SpawnViewGrunt
+
+global function OnSoldierSeeEnemy
+global function TryFriendlyPassingNearby
+
+global function OnSpectreSeeEnemy
+
+global function onlyimc // debug
+global function onlymilitia // debug
+
+global function SetGlobalNPCHealth //debug
+
+
+//=========================================================
+// MP ai soldier
+//
+//=========================================================
+
+struct
+{
+ int militiaTitlesIndex
+ array<string> militiaTitles
+} file
+
+function AiSoldiers_Init()
+{
+ level.COOP_AT_WEAPON_RATES <- {}
+ level.COOP_AT_WEAPON_RATES[ "mp_weapon_rocket_launcher" ] <- 0.5
+ level.COOP_AT_WEAPON_RATES[ "mp_weapon_smr" ] <- 0.4
+ level.COOP_AT_WEAPON_RATES[ "mp_weapon_mgl" ] <- 0.1
+
+ PrecacheSprite( $"sprites/glow_05.vmt" )
+ FlagInit( "disable_npcs" )
+ FlagInit( "Disable_IMC" )
+ FlagInit( "Disable_MILITIA" )
+
+ level.onlySpawn <- null
+
+ level.spectreSpawnStyle <- eSpectreSpawnStyle.MORE_FOR_ENEMY_TITANS
+
+ FlagInit( "AllSpectre" )
+ FlagInit( "AllSpectreIMC" )
+ FlagInit( "AllSpectreMilitia" )
+ FlagInit( "NoSpectreIMC" )
+ FlagInit( "NoSpectreMilitia" )
+
+ RegisterSignal( "OnSendAIToAssaultPoint" )
+
+ InitMilitiaTitles()
+
+ AddCallback_OnClientConnecting( AiSoldiers_InitPlayer )
+
+ if ( GetDeveloperLevel() > 0 )
+ AddClientCommandCallback( "SpawnViewGrunt", ClientCommand_SpawnViewGrunt )
+
+}
+
+bool function ClientCommand_SpawnViewGrunt( entity player, array<string> args )
+{
+ int team = args[0].tointeger()
+ if ( GetDeveloperLevel() < 1 )
+ return true
+
+ vector origin = player.EyePosition()
+ vector angles = player.EyeAngles()
+ vector forward = AnglesToForward( angles )
+ TraceResults result = TraceLine( origin, origin + forward * 2000, player )
+ angles.x = 0
+ angles.z = 0
+
+ entity guy = CreateSoldier( team, result.endPos, angles )
+ DispatchSpawn( guy )
+ return true
+}
+
+// debug commands
+function onlyimc()
+{
+ level.onlySpawn = TEAM_IMC
+ printt( "Only spawning IMC AI" )
+}
+
+// debug commands
+function onlymilitia()
+{
+ level.onlySpawn = TEAM_MILITIA
+ printt( "Only spawning Militia AI" )
+}
+
+//////////////////////////////////////////////////////////
+void function AiSoldiers_InitPlayer( entity player )
+{
+ player.s.next_ai_callout_time <- -1
+
+ string squadName = GetPlayerSpectreSquadName( player )
+ player.p.spectreSquad = squadName
+}
+
+//////////////////////////////////////////////////////////
+string function MakeSquadName( int team, string msg )
+{
+ string teamStr
+
+ if ( team == TEAM_IMC )
+ teamStr = "imc"
+ else if ( team == TEAM_MILITIA )
+ teamStr = "militia"
+ else
+ teamStr = "default"
+
+ return "squad_" + teamStr + msg
+}
+
+//////////////////////////////////////////////////////////
+
+
+//////////////////////////////////////////////////////////
+// common init for grunts and spectres
+void function CommonMinionInit( entity npc )
+{
+ RandomizeHead( npc )
+
+ if ( IsMultiplayer() )
+ {
+ npc.kv.alwaysAlert = 1
+ npc.EnableNPCFlag( NPC_STAY_CLOSE_TO_SQUAD | NPC_NEW_ENEMY_FROM_SOUND )
+ npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+ }
+
+ npc.s.cpState <- eNPCStateCP.NONE
+
+ if ( npc.kv.alwaysalert.tointeger() == 1 )
+ npc.SetDefaultSchedule( "SCHED_ALERT_SCAN" )
+}
+
+function SetupMinionForRPGs( entity soldier )
+{
+ soldier.SetEnemyChangeCallback( OnEnemyChanged_MinionSwitchToHeavyArmorWeapon )
+}
+
+
+void function OnSoldierSeeEnemy( entity guy )
+{
+ guy.EndSignal( "OnDeath" )
+
+ if ( NPC_GruntChatterSPEnabled( guy ) )
+ return
+
+ while ( true )
+ {
+ var results = WaitSignal( guy, "OnSeeEnemy" )
+
+ if ( !IsValid( guy ) )
+ return
+
+ TrySpottedCallout( guy, expect entity( results.activator ) )
+ }
+}
+
+void function TryFriendlyPassingNearby( entity grunt )
+{
+ grunt.EndSignal( "OnDeath" )
+
+ if ( NPC_GruntChatterSPEnabled( grunt ) )
+ return
+
+ while ( true )
+ {
+ wait 5
+
+ if ( !IsValid( grunt ) )
+ return
+
+ #if GRUNT_CHATTER_MP_ENABLED
+ // only do this a minute into the match
+ if ( Time() > 60.0 && TryFriendlyCallout( grunt, "pilot", "bc_reactFriendlyPilot" , 500 ) )
+ continue
+ if ( TryFriendlyCallout( grunt, "titan", "bc_reactTitanfallFriendlyArrives" , 500 ) )
+ continue
+ if ( TryFriendlyCallout( grunt, "npc_super_spectre", "bc_reactReaperFriendlyArrives" , 500 ) )
+ continue
+ if ( TryFriendlyCallout( grunt, "npc_frag_drone", "bc_reactTickSpawnFriendly" , 500 ) )
+ continue
+ if ( IsAlive( grunt.GetEnemy() ) )
+ {
+ entity enemy = grunt.GetEnemy()
+ if ( enemy.IsTitan() )
+ PlayGruntChatterMPLine( grunt, "bc_generalCombatTitan" )
+ else
+ PlayGruntChatterMPLine( grunt, "bc_generalCombat" )
+ }
+ else
+ {
+ PlayGruntChatterMPLine( grunt, "bc_generalNonCombat" )
+ }
+ #endif
+ }
+}
+
+#if GRUNT_CHATTER_MP_ENABLED
+bool function TryFriendlyCallout( entity grunt, string npcClassname, string callout, float dist )
+{
+ array<entity> nearbyFriendlies
+ float distSq = dist*dist
+ if ( npcClassname == "pilot" )
+ {
+ array<entity> players = GetPlayerArrayOfTeam_AlivePilots( grunt.GetTeam() )
+ foreach( p in players )
+ {
+ if ( DistanceSqr( p.GetOrigin(), grunt.GetOrigin() ) > distSq )
+ continue
+ nearbyFriendlies.append( p )
+ }
+ }
+ else if ( npcClassname == "titan" )
+ {
+ nearbyFriendlies = GetNPCArrayEx( "npc_titan", grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), dist )
+ array<entity> players = GetPlayerArrayOfTeam_Alive( grunt.GetTeam() )
+ foreach( p in players )
+ {
+ if ( !p.IsTitan() )
+ continue
+ if ( DistanceSqr( p.GetOrigin(), grunt.GetOrigin() ) > distSq )
+ continue
+ nearbyFriendlies.append( p )
+ }
+ }
+ else
+ {
+ nearbyFriendlies = GetNPCArrayEx( npcClassname, grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), dist )
+ }
+
+ foreach ( friendly in nearbyFriendlies )
+ {
+ if ( !IsAlive( friendly ) )
+ continue
+
+ if ( GetDoomedState( friendly ) )
+ continue
+
+ PlayGruntChatterMPLine( grunt, callout )
+ return true
+ }
+
+ return false
+}
+#endif
+
+void function OnSpectreSeeEnemy( entity guy )
+{
+ guy.EndSignal( "OnDeath" )
+
+ while ( true )
+ {
+ var results = WaitSignal( guy, "OnGainEnemyLOS" )
+
+ TrySpottedCallout( guy, expect entity( results.activator ) )
+ }
+}
+
+
+//////////////////////////////////////////////////////////
+bool function IsValidRocketTarget( entity enemy )
+{
+ return enemy.GetArmorType() == ARMOR_TYPE_HEAVY
+}
+
+//////////////////////////////////////////////////////////
+function DisableMinionUsesHeavyWeapons( entity soldier )
+{
+ soldier.SetEnemyChangeCallback( OnEnemyChanged_MinionUpdateAimSettingsForEnemy )
+}
+
+void function OnEnemyChanged_MinionSwitchToHeavyArmorWeapon( entity soldier )
+{
+ OnEnemyChanged_TryHeavyArmorWeapon( soldier )
+ OnEnemyChanged_MinionUpdateAimSettingsForEnemy( soldier )
+}
+
+//////////////////////////////////////////////////////////
+void function OnEnemyChanged_MinionUpdateAimSettingsForEnemy( entity soldier )
+{
+ SetProficiency( soldier )
+}
+
+
+bool function AssignNPCAppropriateWeaponFromWeapons( entity npc, array<entity> weapons, bool isRocketTarget )
+{
+ // first try to find an appropriate weapon
+ foreach ( weapon in weapons )
+ {
+ bool isAntiTitan = weapon.GetWeaponType() == WT_ANTITITAN
+ if ( isAntiTitan == isRocketTarget )
+ {
+ // found a weapon to use
+ npc.SetActiveWeaponByName( weapon.GetWeaponClassName() )
+ return true
+ }
+ }
+ return false
+}
+
+//////////////////////////////////////////////////////////
+void function OnEnemyChanged_TryHeavyArmorWeapon( entity npc )
+{
+ entity enemy = npc.GetEnemy()
+ if ( !IsAlive( enemy ) )
+ return
+
+ array<entity> weapons = npc.GetMainWeapons()
+
+ // do we have a weapon to switch to?
+ if ( !weapons.len() )
+ return
+
+ entity activeWeapon = npc.GetActiveWeapon()
+ bool isRocketTarget = IsValidRocketTarget( enemy )
+
+ if ( activeWeapon == null )
+ {
+ if ( AssignNPCAppropriateWeaponFromWeapons( npc, weapons, isRocketTarget ) )
+ return
+
+ // if that fails, use the first weapon, so we do consistent behavior
+ npc.SetActiveWeaponByName( weapons[0].GetWeaponClassName() )
+ return
+ }
+
+ bool isActiveWeapon_AntiTitan = activeWeapon.GetWeaponType() == WT_ANTITITAN
+
+ // already using an appropriate weapon?
+ if ( isActiveWeapon_AntiTitan == isRocketTarget )
+ return
+
+ AssignNPCAppropriateWeaponFromWeapons( npc, weapons, isRocketTarget )
+}
+
+const float NPC_CLOSE_DISTANCE_SQR_THRESHOLD = 1000.0 * 1000.0
+
+//////////////////////////////////////////////////////////
+void function TrySpottedCallout( entity guy, entity enemy )
+{
+ if ( !IsAlive( guy ) )
+ return
+
+ if ( !IsAlive( enemy ) )
+ return
+
+ float distanceSqr = DistanceSqr( guy.GetOrigin(), enemy.GetOrigin() )
+ bool isClose = distanceSqr <= NPC_CLOSE_DISTANCE_SQR_THRESHOLD
+
+ if ( enemy.IsTitan() )
+ {
+ if ( IsSpectre( guy ) ) //Spectre callouts
+ {
+ #if SPECTRE_CHATTER_MP_ENABLED
+ PlaySpectreChatterMPLine( guy, "diag_imc_spectre_gs_spotclosetitancall_01" )
+ #else
+ if ( isClose )
+ PlaySpectreChatterToAll( "spectre_gs_spotclosetitancall_01", guy )
+ else
+ PlaySpectreChatterToAll( "spectre_gs_spotfartitan_1_1", guy )
+ #endif
+
+ }
+ else //Grunt callouts
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ PlayGruntChatterMPLine( guy, "bc_enemytitanspotcall" )
+ #endif
+ }
+ }
+ else if ( enemy.IsPlayer() )
+ {
+ if ( IsSpectre( guy ) ) //Spectre callouts
+ {
+ #if SPECTRE_CHATTER_MP_ENABLED
+ PlaySpectreChatterMPLine( guy, "diag_imc_spectre_gs_engagepilotenemy_01_1" )
+ #else
+ if ( isClose )
+ PlaySpectreChatterToAll( "spectre_gs_engagepilotenemy_01_1", guy )
+ else
+ PlaySpectreChatterToAll( "spectre_gs_spotenemypilot_01_1", guy )
+ #endif
+ }
+ else //Grunt callouts
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ if ( isClose )
+ PlayGruntChatterMPLine( guy, "bc_spotenemypilot" )
+ else
+ PlayGruntChatterMPLine( guy, "bc_engagepilotenemy" )
+ #endif
+ }
+ }
+ else if ( IsSuperSpectre( enemy ) )
+ {
+ if ( !IsSpectre( guy ) ) //Spectre callouts
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ PlayGruntChatterMPLine( guy, "bc_reactEnemyReaper" )
+ #endif
+ }
+ }
+ else
+ {
+ if ( !IsSpectre( guy ) ) //Spectre callouts
+ {
+ #if GRUNT_CHATTER_MP_ENABLED
+ PlayGruntChatterMPLine( guy, "bc_reactEnemySpotted" )
+ #endif
+ }
+ }
+}
+
+
+//////////////////////////////////////////////////////////
+string function GetPlayerSpectreSquadName( entity player )
+{
+ return "player" + player.entindex() + "spectreSquad"
+}
+
+
+//////////////////////////////////////////////////////////
+
+string function GetMilitiaTitle()
+{
+ file.militiaTitlesIndex++
+ if ( file.militiaTitlesIndex >= file.militiaTitles.len() )
+ file.militiaTitlesIndex = 0
+
+ return file.militiaTitles[ file.militiaTitlesIndex ]
+}
+
+void function InitMilitiaTitles()
+{
+ file.militiaTitles = [
+ "#NPC_MILITIA_NAME_AND_RANK_0",
+ "#NPC_MILITIA_NAME_AND_RANK_1",
+ "#NPC_MILITIA_NAME_AND_RANK_2",
+ "#NPC_MILITIA_NAME_AND_RANK_3",
+ "#NPC_MILITIA_NAME_AND_RANK_4",
+ "#NPC_MILITIA_NAME_AND_RANK_5",
+ "#NPC_MILITIA_NAME_AND_RANK_6",
+ "#NPC_MILITIA_NAME_AND_RANK_7",
+ "#NPC_MILITIA_NAME_AND_RANK_8",
+ "#NPC_MILITIA_NAME_AND_RANK_9",
+ "#NPC_MILITIA_NAME_AND_RANK_10",
+ "#NPC_MILITIA_NAME_AND_RANK_11",
+ "#NPC_MILITIA_NAME_AND_RANK_12",
+ "#NPC_MILITIA_NAME_AND_RANK_13",
+ "#NPC_MILITIA_NAME_AND_RANK_14",
+ "#NPC_MILITIA_NAME_AND_RANK_15",
+ "#NPC_MILITIA_NAME_AND_RANK_16",
+ "#NPC_MILITIA_NAME_AND_RANK_17",
+ "#NPC_MILITIA_NAME_AND_RANK_18",
+ "#NPC_MILITIA_NAME_AND_RANK_19",
+ "#NPC_MILITIA_NAME_AND_RANK_20",
+ "#NPC_MILITIA_NAME_AND_RANK_21",
+ "#NPC_MILITIA_NAME_AND_RANK_22",
+ "#NPC_MILITIA_NAME_AND_RANK_23",
+ "#NPC_MILITIA_NAME_AND_RANK_24",
+ "#NPC_MILITIA_NAME_AND_RANK_25",
+ "#NPC_MILITIA_NAME_AND_RANK_26",
+ "#NPC_MILITIA_NAME_AND_RANK_27",
+ "#NPC_MILITIA_NAME_AND_RANK_28",
+ "#NPC_MILITIA_NAME_AND_RANK_29",
+ "#NPC_MILITIA_NAME_AND_RANK_30",
+ "#NPC_MILITIA_NAME_AND_RANK_31",
+ "#NPC_MILITIA_NAME_AND_RANK_32",
+ "#NPC_MILITIA_NAME_AND_RANK_33",
+ "#NPC_MILITIA_NAME_AND_RANK_34",
+ "#NPC_MILITIA_NAME_AND_RANK_35",
+ "#NPC_MILITIA_NAME_AND_RANK_36",
+ "#NPC_MILITIA_NAME_AND_RANK_37",
+ "#NPC_MILITIA_NAME_AND_RANK_38",
+ "#NPC_MILITIA_NAME_AND_RANK_39",
+ "#NPC_MILITIA_NAME_AND_RANK_40",
+ "#NPC_MILITIA_NAME_AND_RANK_41",
+ "#NPC_MILITIA_NAME_AND_RANK_42",
+ "#NPC_MILITIA_NAME_AND_RANK_43",
+ "#NPC_MILITIA_NAME_AND_RANK_44",
+ "#NPC_MILITIA_NAME_AND_RANK_45",
+ "#NPC_MILITIA_NAME_AND_RANK_46",
+ "#NPC_MILITIA_NAME_AND_RANK_47",
+ "#NPC_MILITIA_NAME_AND_RANK_48",
+ "#NPC_MILITIA_NAME_AND_RANK_49",
+ "#NPC_MILITIA_NAME_AND_RANK_50",
+ "#NPC_MILITIA_NAME_AND_RANK_51",
+ "#NPC_MILITIA_NAME_AND_RANK_52",
+ "#NPC_MILITIA_NAME_AND_RANK_53",
+ "#NPC_MILITIA_NAME_AND_RANK_54",
+ "#NPC_MILITIA_NAME_AND_RANK_55",
+ "#NPC_MILITIA_NAME_AND_RANK_56",
+ "#NPC_MILITIA_NAME_AND_RANK_57",
+ "#NPC_MILITIA_NAME_AND_RANK_58",
+ "#NPC_MILITIA_NAME_AND_RANK_59",
+ "#NPC_MILITIA_NAME_AND_RANK_60",
+ "#NPC_MILITIA_NAME_AND_RANK_61",
+ "#NPC_MILITIA_NAME_AND_RANK_62",
+ "#NPC_MILITIA_NAME_AND_RANK_63",
+ "#NPC_MILITIA_NAME_AND_RANK_64",
+ "#NPC_MILITIA_NAME_AND_RANK_65",
+ "#NPC_MILITIA_NAME_AND_RANK_66",
+ "#NPC_MILITIA_NAME_AND_RANK_67",
+ "#NPC_MILITIA_NAME_AND_RANK_68",
+ "#NPC_MILITIA_NAME_AND_RANK_69",
+ "#NPC_MILITIA_NAME_AND_RANK_70",
+ "#NPC_MILITIA_NAME_AND_RANK_71",
+ "#NPC_MILITIA_NAME_AND_RANK_72",
+ "#NPC_MILITIA_NAME_AND_RANK_73",
+ "#NPC_MILITIA_NAME_AND_RANK_74",
+ "#NPC_MILITIA_NAME_AND_RANK_75",
+ "#NPC_MILITIA_NAME_AND_RANK_76",
+ "#NPC_MILITIA_NAME_AND_RANK_77",
+ "#NPC_MILITIA_NAME_AND_RANK_78",
+ "#NPC_MILITIA_NAME_AND_RANK_79",
+ "#NPC_MILITIA_NAME_AND_RANK_80",
+ "#NPC_MILITIA_NAME_AND_RANK_81",
+ "#NPC_MILITIA_NAME_AND_RANK_82",
+ "#NPC_MILITIA_NAME_AND_RANK_83",
+ "#NPC_MILITIA_NAME_AND_RANK_84",
+ "#NPC_MILITIA_NAME_AND_RANK_85",
+ "#NPC_MILITIA_NAME_AND_RANK_86",
+ "#NPC_MILITIA_NAME_AND_RANK_87",
+ "#NPC_MILITIA_NAME_AND_RANK_88",
+ "#NPC_MILITIA_NAME_AND_RANK_89",
+ "#NPC_MILITIA_NAME_AND_RANK_90",
+ "#NPC_MILITIA_NAME_AND_RANK_91",
+ "#NPC_MILITIA_NAME_AND_RANK_92",
+ "#NPC_MILITIA_NAME_AND_RANK_93",
+ "#NPC_MILITIA_NAME_AND_RANK_94",
+ "#NPC_MILITIA_NAME_AND_RANK_95"
+ "#NPC_MILITIA_NAME_AND_RANK_96",
+ "#NPC_MILITIA_NAME_AND_RANK_97",
+ "#NPC_MILITIA_NAME_AND_RANK_98",
+ "#NPC_MILITIA_NAME_AND_RANK_99"
+ ]
+
+ file.militiaTitles.randomize()
+ file.militiaTitlesIndex = 0
+}
+
+//////////////////////////////////////////////////////////
+function disable_npcs()
+{
+ FlagSet( "disable_npcs" )
+ printl( "disabling_npcs" )
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetClassName() == "npc_turret_mega" )
+ continue
+ if ( guy.GetClassName() == "npc_turret_sentry" )
+ continue
+ if ( guy.GetClassName() == "npc_titan" )
+ continue
+
+ guy.Destroy()
+ }
+}
+//////////////////////////////////////////////////////////
+// //hack - we want to toggle new AI on and off through the dev menu even though playlist defaults to use them all the time
+function disable_new_npcs()
+{
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetClassName() == "npc_turret_mega" )
+ continue
+ if ( guy.GetClassName() == "npc_turret_sentry" )
+ continue
+ if ( guy.GetClassName() == "npc_titan" )
+ continue
+
+ guy.Destroy()
+ }
+}
+
+function ResetNPCs()
+{
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetClassName() == "npc_turret_mega" )
+ continue
+ if ( guy.GetClassName() == "npc_turret_sentry" )
+ continue
+
+ if ( guy.GetClassName() == "npc_titan" && IsValid( guy.GetTitanSoul() ) )
+ {
+ guy.GetTitanSoul().Destroy()
+ }
+
+ guy.Destroy()
+ }
+}
+
+//////////////////////////////////////////////////////////
+function Disable_IMC()
+{
+ DisableAutoPopulate( TEAM_IMC )
+ printl( "Disable_IMC" )
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetTeam() == TEAM_IMC )
+ guy.Kill_Deprecated_UseDestroyInstead()
+ }
+}
+
+
+//////////////////////////////////////////////////////////
+function Disable_MILITIA()
+{
+ DisableAutoPopulate( TEAM_MILITIA )
+ printl( "Disable_MILITIA" )
+ array<entity> guys = GetNPCArray()
+ foreach ( guy in guys )
+ {
+ if ( guy.GetTeam() == TEAM_MILITIA )
+ guy.Kill_Deprecated_UseDestroyInstead()
+ }
+}
+
+//////////////////////////////////////////////////////////
+function IsNPCSpawningEnabled()
+{
+ if ( Riff_AllowNPCs() != eAllowNPCs.Default )
+ {
+ if ( Riff_AllowNPCs() == eAllowNPCs.None )
+ return false
+
+ return true
+ }
+
+ return true
+}
+
+
+function DisableAutoPopulate( team )
+{
+ switch ( team )
+ {
+ case TEAM_IMC:
+ FlagSet( "Disable_IMC" )
+ break
+
+ case TEAM_MILITIA:
+ FlagSet( "Disable_MILITIA" )
+ break
+
+ default:
+ Assert( 0, "team number " + team + " not setup for autoPopulation.")
+ break
+ }
+}
+
+function EnableAutoPopulate( team )
+{
+ switch ( team )
+ {
+ case TEAM_IMC:
+ FlagClear( "Disable_IMC" )
+ break
+
+ case TEAM_MILITIA:
+ FlagClear( "Disable_MILITIA" )
+ break
+
+ default:
+ Assert( 0, "team number " + team + " not setup for autoPopulation.")
+ break
+ }
+}
+
+//////////////////////////////////////////////////////////
+
+
+function GuyTeleportsOnPathFail( guy, origin )
+{
+ expect entity( guy )
+
+ guy.EndSignal( "OnFailedToPath" )
+
+ local e = {}
+ e.waited <- false
+ OnThreadEnd(
+ function() : ( guy, origin, e )
+ {
+ if ( !IsAlive( guy ) )
+ return
+
+ // wait was cut off
+ if ( !e.waited )
+ guy.SetOrigin( origin )
+ }
+ )
+
+ wait 2
+ e.waited = true
+}
+
+void function SquadAssaultOrigin( array<entity> group, vector origin, float radius = STANDARDGOALRADIUS )
+{
+ foreach ( member in group )
+ {
+ thread AssaultOrigin( member, origin, radius )
+ }
+}
+
+void function AssaultOrigin( entity guy, vector origin, float radius = STANDARDGOALRADIUS )
+{
+ waitthread SendAIToAssaultPoint( guy, origin, <0,0,0>, radius )
+}
+
+void function SendAIToAssaultPoint( entity guy, vector origin, vector angles, float radius = STANDARDGOALRADIUS )
+{
+ Assert( IsAlive( guy ) )
+ guy.Signal( "OnSendAIToAssaultPoint" )
+ guy.Anim_Stop() // in case we were doing an anim already
+ guy.EndSignal( "OnDeath" )
+ guy.EndSignal( "OnSendAIToAssaultPoint" )
+
+ bool allowFlee = guy.GetNPCFlag( NPC_ALLOW_FLEE )
+ bool allowHandSignal = guy.GetNPCFlag( NPC_ALLOW_HAND_SIGNALS )
+
+ OnThreadEnd(
+ function() : ( guy, allowFlee, allowHandSignal )
+ {
+ if ( IsAlive( guy ) )
+ {
+ guy.SetNPCFlag( NPC_ALLOW_FLEE, allowFlee )
+ guy.SetNPCFlag( NPC_ALLOW_HAND_SIGNALS, allowHandSignal )
+ }
+ }
+ )
+
+ guy.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS )
+ guy.AssaultPoint( origin )
+ guy.AssaultSetGoalRadius( radius )
+ guy.WaitSignal( "OnFinishedAssault" )
+
+}
+
+function SetGlobalNPCHealth( healthValue ) //Debug, for trailer team
+{
+ array<entity> npcArray = GetNPCArray()
+
+ foreach ( npc in npcArray )
+ {
+ npc.SetMaxHealth( healthValue )
+ npc.SetHealth( healthValue )
+ }
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_mp.gnut
new file mode 100644
index 000000000..37b891699
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_mp.gnut
@@ -0,0 +1 @@
+//fuck \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_sp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_sp.gnut
new file mode 100644
index 000000000..6faf66491
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers_sp.gnut
@@ -0,0 +1,17 @@
+global function IsAutoPopulateEnabled
+
+bool function IsAutoPopulateEnabled( var team = null )
+{
+ if ( IsNPCSpawningEnabled() == false )
+ return false
+
+ if ( Flag( "disable_npcs" ) )
+ return false
+
+ if ( team == TEAM_MILITIA && Flag( "Disable_MILITIA" ) )
+ return false
+ if ( team == TEAM_IMC && Flag( "Disable_IMC" ) )
+ return false
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn.gnut
new file mode 100644
index 000000000..7e4d2cddf
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn.gnut
@@ -0,0 +1,696 @@
+untyped
+
+global function AiSpawn_Init
+
+global function __GetWeaponModel
+global function AssaultLinkedMoveTarget
+global function AssaultMoveTarget
+global function AutoSquadnameAssignment
+global function CreateArcTitan
+global function CreateAtlas
+global function CreateElitePilot
+global function CreateElitePilotAssassin
+global function CreateFragDrone
+global function CreateFragDroneCan
+global function CreateGenericDrone
+global function CreateGunship
+global function CreateHenchTitan
+global function CreateMarvin
+global function CreateNPC
+global function CreateNPCFromAISettings
+global function CreateNPCTitan
+global function CreateOgre
+global function CreateProwler
+global function CreateRocketDrone
+global function CreateRocketDroneGrunt
+global function CreateShieldDrone
+global function CreateShieldDroneGrunt
+global function CreateSoldier
+global function CreateSpectre
+global function CreateStalker
+global function CreateStryder
+global function CreateSuperSpectre
+global function CreateWorkerDrone
+global function CreateZombieStalker
+global function CreateZombieStalkerMossy
+global function StopAssaultMoveTarget
+
+global const HACK_CAP_BACK1 = $"models/sandtrap/sandtrap_wall_bracket.mdl"
+global const HACK_CAP_BACK2 = $"models/pipes/pipe_modular_grey_bracket_cap.mdl"
+global const HACK_CAP_BACK3 = $"models/lamps/office_lights_hanging_wire.mdl"
+global const HACK_DRONE_BACK1 = $"models/Weapons/ammoboxes/backpack_single.mdl"
+global const HACK_DRONE_BACK2 = $"models/barriers/fence_wire_holder_double.mdl"
+global const DEFAULT_TETHER_RADIUS = 1500
+global const DEFAULT_COVER_BEHAVIOR_CYLINDER_HEIGHT = 512
+
+struct
+{
+ array<string> moveTargetClasses
+} file
+
+void function AiSpawn_Init()
+{
+ PrecacheModel( HACK_CAP_BACK1 )
+ PrecacheModel( HACK_CAP_BACK2 )
+ PrecacheModel( HACK_CAP_BACK3 )
+ PrecacheModel( HACK_DRONE_BACK1 )
+ PrecacheModel( HACK_DRONE_BACK2 )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_LMG )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_RIFLE )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_ROCKET )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_SHOTGUN )
+ PrecacheModel( TEAM_IMC_GRUNT_MODEL_SMG )
+
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_LMG )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_RIFLE )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_ROCKET )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_SHOTGUN )
+ PrecacheModel( TEAM_MIL_GRUNT_MODEL_SMG )
+
+ file.moveTargetClasses = [ "info_move_target", "info_move_animation" ]
+ foreach ( movetargetClass in file.moveTargetClasses )
+ {
+ AddSpawnCallbackEditorClass( "info_target", movetargetClass, InitInfoMoveTargetFlags )
+ }
+
+ RegisterSignal( "StopAssaultMoveTarget" )
+ RegisterSignal( "OnFinishedAssaultChain" )
+
+ AiSpawnContent_Init()
+ #if DEV
+ // just to insure that ai settings are being setup properly.
+ InitNpcSettingsFileNamesForDevMenu()
+ SetupSpawnAIButtons( TEAM_MILITIA )
+ AddCallback_EntitiesDidLoad( AiSpawn_EntitiesDidLoad )
+ #endif
+}
+
+void function AiSpawn_EntitiesDidLoad()
+{
+ #if DEV
+ // On load in dev, verify that subclass matches leveled_aisettings. Subclass is being eradicated.
+ foreach ( spawner in GetSpawnerArrayByClassName( "npc_titan" ) )
+ {
+ table spawnerKeyValues = spawner.GetSpawnEntityKeyValues()
+ if ( "model" in spawnerKeyValues )
+ {
+ switch ( spawnerKeyValues.model.tolower() )
+ {
+ case "models/titans/atlas/atlas_titan.mdl":
+ case "models/titans/ogre/ogre_titan.mdl":
+ case "models/titans/stryder/stryder_titan.mdl":
+ CodeWarning( "Titan has deprecated model at " + spawnerKeyValues.origin )
+ break
+ }
+ }
+ }
+
+ foreach ( model in GetEntArrayByClass_Expensive( "prop_dynamic" ) )
+ {
+ switch ( model.GetModelName() )
+ {
+ case $"models/titans/atlas/atlas_titan.mdl":
+ case $"models/titans/ogre/ogre_titan.mdl":
+ case $"models/titans/stryder/stryder_titan.mdl":
+ CodeWarning( "Prop has deprecated model at " + model.GetOrigin() )
+ break
+ }
+ }
+
+ if ( IsSingleplayer() )
+ {
+ foreach ( spawner in GetSpawnerArrayByClassName( "npc_titan" ) )
+ {
+ table kvs = spawner.GetSpawnEntityKeyValues()
+ vector origin = StringToVector( expect string( kvs.origin ) )
+ if ( !( "leveled_aisettings" in kvs ) )
+ {
+ CodeWarning( "Titan Spawner at " + origin + " has no leveled_aisettings" )
+ continue
+ }
+
+ string aiSettings = expect string( kvs.leveled_aisettings )
+ string playerSettings = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+ string playerModel = expect string( GetPlayerSettingsFieldForClassName( playerSettings, "bodymodel" ) )
+ string npcModel = expect string( kvs.model )
+ if ( npcModel != playerModel )
+ CodeWarning( "Titan spawner at " + origin + " has model " + npcModel + " that does not match player settings model " + playerModel )
+ }
+ }
+
+ #endif
+
+ table<entity, bool> foundSpawners
+ // precache weapons from the AI
+ foreach ( aiSettings in GetAllNPCSettings() )
+ {
+ // any of these spawned in the level?
+ string baseClass = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "BaseClass" ) )
+ array<entity> spawners = GetSpawnerArrayByClassName( baseClass )
+
+ foreach ( spawner in spawners )
+ {
+ if ( spawner in foundSpawners )
+ continue
+ foundSpawners[ spawner ] <- true
+ // this may be set on the entity in leveled
+ table kvs = spawner.GetSpawnEntityKeyValues()
+ if ( !( "subclass" in kvs ) )
+ continue
+
+ string origin = expect string( spawner.GetSpawnEntityKeyValues().origin )
+ string subclass = expect string( spawner.GetSpawnEntityKeyValues().subclass )
+ CodeWarning( "NPC spawner at " + origin + " has subclass " + subclass + ". Replace deprecated subclass key with leveled_aisettings." )
+ }
+ }
+
+}
+
+const ESCALATION_INCOMBAT_TIMEOUT = 180
+const ESCALATION_FRACTION_DEAD = 0.5
+
+
+/************************************************************************************************\
+
+######## #### ######## ### ## ##
+ ## ## ## ## ## ### ##
+ ## ## ## ## ## #### ##
+ ## ## ## ## ## ## ## ##
+ ## ## ## ######### ## ####
+ ## ## ## ## ## ## ###
+ ## #### ## ## ## ## ##
+
+\************************************************************************************************/
+
+//////////////////////////////////////////////////////////
+
+entity function CreateHenchTitan( string titanType, vector origin, vector angles )
+{
+ entity npc = CreateNPCTitan( titanType, TEAM_IMC, origin, angles, [] )
+ string settings = expect string( Dev_GetPlayerSettingByKeyField_Global( titanType, "sp_aiSettingsFile" ) )
+ SetSpawnOption_AISettings( npc, settings )
+ SetSpawnOption_Titanfall( npc )
+ SetSpawnOption_Alert( npc )
+ SetSpawnOption_NPCTitan( npc, TITAN_HENCH )
+ npc.ai.titanSpawnLoadout.setFile = titanType
+ OverwriteLoadoutWithDefaultsForSetFile( npc.ai.titanSpawnLoadout )
+ return npc
+}
+
+entity function CreateAtlas( int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateNPCTitan( "titan_atlas", team, origin, angles, settingsMods )
+ SetSpawnOption_AISettings( npc, "npc_titan_atlas" )
+ return npc
+}
+
+entity function CreateStryder( int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateNPCTitan( "titan_stryder", team, origin, angles, settingsMods )
+ SetSpawnOption_AISettings( npc, "npc_titan_stryder" )
+ return npc
+}
+
+entity function CreateOgre( int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateNPCTitan( "titan_ogre", team, origin, angles, settingsMods )
+ SetSpawnOption_AISettings( npc, "npc_titan_ogre" )
+ return npc
+}
+
+entity function CreateArcTitan( int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateNPCTitan( "titan_stryder", team, origin, angles, settingsMods )
+ SetSpawnOption_AISettings( npc, "npc_titan_arc" )
+ return npc
+}
+
+entity function CreateNPCTitan( string settings, int team, vector origin, vector angles, array<string> settingsMods = [] )
+{
+ entity npc = CreateEntity( "npc_titan" )
+ npc.kv.origin = origin
+ npc.kv.angles = Vector( 0, angles.y, 0 )
+ npc.kv.teamnumber = team
+ SetTitanSettings( npc.ai.titanSettings, settings, settingsMods )
+ return npc
+}
+
+entity function CreateSpectre( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_spectre", team, origin, angles )
+}
+
+entity function CreateStalker( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_stalker", team, origin, angles )
+}
+
+entity function CreateZombieStalker( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_stalker", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_stalker_zombie" )
+ return npc
+}
+
+entity function CreateZombieStalkerMossy( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_stalker", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_stalker_zombie_mossy" )
+ return npc
+}
+
+entity function CreateSuperSpectre( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_super_spectre", team, origin, angles )
+}
+
+entity function CreateGunship( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_gunship", team, origin, angles )
+}
+
+entity function CreateSoldier( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_soldier", team, origin, angles )
+}
+
+entity function CreateProwler( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_prowler", team, origin, angles )
+}
+
+entity function CreateRocketDroneGrunt( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_soldier", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_soldier_drone_summoner_rocket" )
+ return npc
+}
+
+entity function CreateShieldDroneGrunt( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_soldier", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_soldier_drone_summoner" )
+ return npc
+}
+
+entity function CreateElitePilot( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_pilot_elite", team, origin, angles )
+}
+
+entity function CreateElitePilotAssassin( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_pilot_elite", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_pilot_elite_assassin" )
+ return npc
+}
+
+entity function CreateFragDrone( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_frag_drone", team, origin, angles )
+}
+
+entity function CreateFragDroneCan( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_frag_drone", team, origin, angles )
+ npc.ai.fragDroneArmed = false
+ return npc
+}
+
+entity function CreateRocketDrone( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_drone_rocket" )
+ return npc
+}
+
+entity function CreateShieldDrone( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_drone_shield" )
+ return npc
+}
+
+entity function CreateGenericDrone( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_drone" )
+ return npc
+}
+
+entity function CreateWorkerDrone( int team, vector origin, vector angles )
+{
+ entity npc = CreateNPC( "npc_drone", team, origin, angles )
+ SetSpawnOption_AISettings( npc, "npc_drone_worker" )
+ return npc
+}
+
+entity function CreateMarvin( int team, vector origin, vector angles )
+{
+ return CreateNPC( "npc_marvin", team, origin, angles )
+}
+
+entity function CreateNPC( baseClass, team, origin, angles )
+{
+ entity npc = CreateEntity( expect string( baseClass ) )
+ npc.kv.teamnumber = team
+ npc.kv.origin = origin
+ npc.kv.angles = angles
+
+ return npc
+}
+
+entity function CreateNPCFromAISettings( string aiSettings, int team, vector origin, vector angles )
+{
+ string baseClass = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "BaseClass" ) )
+ entity npc = CreateNPC( baseClass, team, origin, angles )
+ SetSpawnOption_AISettings( npc, aiSettings )
+ return npc
+}
+
+
+
+/************************************************************************************************\
+
+ ###### ####### ## ## ## ## ####### ## ##
+## ## ## ## ### ### ### ### ## ## ### ##
+## ## ## #### #### #### #### ## ## #### ##
+## ## ## ## ### ## ## ### ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ####
+## ## ## ## ## ## ## ## ## ## ## ###
+ ###### ####### ## ## ## ## ####### ## ##
+
+\************************************************************************************************/
+
+entity function GetTargetOrLink( entity npc )
+{
+ string target = npc.GetTarget_Deprecated()
+ if ( target != "" )
+ return GetEnt( target )
+
+ array<entity> links = npc.GetLinkEntArray()
+ if ( links.len() )
+ return links.getrandom()
+
+ return null
+}
+
+bool function IsMoveTarget( entity ent )
+{
+ if ( !ent.HasKey( "editorclass" ) )
+ return false
+
+ string editorClass = expect string( ent.kv.editorclass )
+ foreach ( moveTargetClass in file.moveTargetClasses )
+ {
+ if ( editorClass == moveTargetClass )
+ return true
+ }
+ return false
+}
+
+bool function IsPotentialThreatTarget( entity ent )
+{
+ if ( !ent.HasKey( "editorclass" ) )
+ return false
+
+ string editorClass = expect string( ent.kv.editorclass )
+ if ( editorClass == "info_potential_threat_target" )
+ return true
+
+ return false
+}
+
+function AssaultLinkedMoveTarget( entity npc )
+{
+ entity ent = GetTargetOrLink( npc )
+ if ( ent == null )
+ return
+ if ( !IsMoveTarget( ent ) )
+ return
+
+ AssaultMoveTarget( npc, ent )
+}
+
+function AssaultMoveTarget( entity npc, entity ent )
+{
+ npc.EndSignal( "OnDeath" )
+ npc.EndSignal( "OnDestroy" )
+ npc.EndSignal( "StopAssaultMoveTarget" )
+ ent.EndSignal( "OnDestroy" )
+
+ Assert( IsMoveTarget( ent ) )
+
+ OnThreadEnd(
+ function() : ( npc )
+ {
+ if ( IsAlive( npc ) )
+ {
+ Signal( npc, "OnFinishedAssaultChain" )
+ }
+ }
+ )
+
+ for ( ;; )
+ {
+ vector origin = ent.GetOrigin()
+ vector angles = ent.GetAngles()
+ float radius = 750
+ float height = 750
+
+ if ( ent.HasKey( "script_predelay" ) )
+ {
+ float time = float( ent.GetValueForKey( "script_predelay" ) )
+ if ( time > 0.0 )
+ wait time
+ }
+
+ if ( ent.HasKey( "script_goal_radius" ) )
+ radius = float( ent.kv.script_goal_radius )
+
+ if ( ent.HasKey( "script_goal_height" ) )
+ height = float( ent.kv.script_goal_height )
+
+ npc.AssaultPointClamped( origin )
+ npc.AssaultSetGoalRadius( radius )
+ npc.AssaultSetGoalHeight( height )
+
+ if ( ent.HasKey( "face_angles" ) && ent.kv.face_angles == "1" )
+ npc.AssaultSetAngles( angles, true )
+
+ if ( ent.HasKey( "script_fight_radius" ) )
+ {
+ float fightRadius = float( ent.kv.script_fight_radius )
+ npc.AssaultSetFightRadius( fightRadius )
+ }
+
+ if ( npc.IsLinkedToEnt( ent ) && ent.HasKey( "unlink" ) && ent.kv.unlink == "1" )
+ npc.UnlinkFromEnt( ent )
+
+ array<entity> entChildren = ent.GetLinkEntArray()
+
+ bool finalDestination = entChildren.len() == 0
+ npc.AssaultSetFinalDestination( finalDestination ) // this doesn't seem to make any difference as far as I can tell. Bug #117062
+
+ if ( ent.HasKey( "clear_potential_threat_pos" ) && int( ent.kv.clear_potential_threat_pos ) == 1 )
+ npc.ClearPotentialThreatPos()
+
+ foreach ( ent in entChildren )
+ {
+ if ( IsPotentialThreatTarget( ent ) )
+ {
+ npc.SetPotentialThreatPos( ent.GetOrigin() )
+ break
+ }
+ }
+
+ table results
+
+ bool skipRunto = ent.HasKey( "skip_runto" ) && int( ent.kv.skip_runto ) == 1
+ if ( !skipRunto )
+ {
+ // If pathing fails we retry waiting for the other signals for 3 seconds.
+ // This solves an issue with npc that failed to path because they where falling.
+
+ const float RETRY_TIME = 3.0
+ float waitStartTime = Time()
+
+ while( true )
+ {
+ // activator, caller, self, signal, value
+ results = WaitSignal( npc, "OnFinishedAssault", "OnEnterGoalRadius", "OnFailedToPath" )
+
+ if ( results.signal != "OnFailedToPath" || waitStartTime + RETRY_TIME < Time() )
+ break
+ }
+ }
+
+ if ( ent.HasKey( "scr_signal" ) )
+ Signal( npc, ent.GetValueForKey( "scr_signal" ), { nodeSignal = results.signal, node = ent } )
+
+ if ( ent.HasKey( "leveled_animation" ) )
+ {
+ string animation = expect string( ent.kv.leveled_animation )
+ Assert( npc.Anim_HasSequence( animation ), "Npc " + npc + " with model " + npc.GetModelName() + " does not have animation sequence " + animation )
+ if ( skipRunto )
+ waitthread PlayAnimTeleport( npc, animation, ent )
+ else
+ waitthread PlayAnimRun( npc, animation, ent, false )
+ }
+
+ if ( ent.HasKey( "scr_flag_set" ) )
+ FlagSet( ent.GetValueForKey( "scr_flag_set" ) )
+
+ if ( ent.HasKey( "scr_flag_clear" ) )
+ FlagClear( ent.GetValueForKey( "scr_flag_clear" ) )
+
+ if ( ent.HasKey( "scr_flag_wait" ) )
+ FlagWait( ent.GetValueForKey( "scr_flag_wait" ) )
+
+ if ( ent.HasKey( "scr_flag_wait_clear" ) )
+ FlagWaitClear( ent.GetValueForKey( "scr_flag_wait_clear" ) )
+
+ if ( ent.HasKey( "path_wait" ) )
+ {
+ float time = float( ent.GetValueForKey( "path_wait" ) )
+ if ( time > 0.0 )
+ wait time
+ }
+
+ if ( ent.HasKey( "disable_assault_on_goal" ) && int( ent.kv.disable_assault_on_goal ) == 1 )
+ npc.DisableBehavior( "Assault" )
+
+ if ( entChildren.len() == 0 )
+ return
+
+ entChildren.randomize()
+ ent = null
+ foreach ( child in entChildren )
+ {
+ if ( IsMoveTarget( child ) )
+ {
+ ent = child
+ break
+ }
+ }
+
+ if ( ent == null )
+ return
+ }
+}
+
+void function StopAssaultMoveTarget( entity npc )
+{
+ npc.Signal( "StopAssaultMoveTarget" )
+}
+
+void function InitInfoMoveTargetFlags( entity infoMoveTarget )
+{
+ #if DEV
+ if ( infoMoveTarget.HasKey( "script_goal_radius" ) )
+ {
+ int radius = int( infoMoveTarget.kv.script_goal_radius )
+ if ( radius < 64 )
+ CodeWarning( "move target at " + infoMoveTarget.GetOrigin() + " had goal radius " + radius + " which is less than minimum 64" )
+ }
+ #endif
+ if ( infoMoveTarget.HasKey( "scr_flag_set" ) )
+ FlagInit( infoMoveTarget.GetValueForKey( "scr_flag_set" ) )
+ if ( infoMoveTarget.HasKey( "scr_flag_clear" ) )
+ FlagInit( infoMoveTarget.GetValueForKey( "scr_flag_clear" ) )
+ if ( infoMoveTarget.HasKey( "scr_flag_wait" ) )
+ FlagInit( infoMoveTarget.GetValueForKey( "scr_flag_wait" ) )
+ if ( infoMoveTarget.HasKey( "scr_flag_wait_clear" ) )
+ FlagInit( infoMoveTarget.GetValueForKey( "scr_flag_wait_clear" ) )
+
+ if ( infoMoveTarget.HasKey( "scr_signal" ) )
+ RegisterSignal( infoMoveTarget.GetValueForKey( "scr_signal" ) )
+}
+
+/************************************************************************************************\
+
+######## ####### ####### ## ######
+ ## ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ######
+ ## ## ## ## ## ## ##
+ ## ## ## ## ## ## ## ##
+ ## ####### ####### ######## ######
+
+\************************************************************************************************/
+asset function __GetWeaponModel( weapon )
+{
+ switch ( weapon )
+ {
+ case "mp_weapon_rspn101":
+ return $"models/weapons/rspn101/r101_ab_01.mdl"//$"models/weapons/rspn101/w_rspn101.mdl" --> this is the one I want to spawn, but I get a vague code error when I try
+ break
+
+ default:
+ Assert( 0, "weapon: " + weapon + " not handled to return a model" )
+ break
+ }
+ unreachable
+}
+
+void function AutoSquadnameAssignment( entity npc )
+{
+ int team = npc.GetTeam()
+ switch ( npc.GetClassName() )
+ {
+ case "npc_turret_sentry":
+ case "npc_turret_mega":
+ case "npc_dropship":
+ case "npc_dropship_hero":
+ return
+ }
+
+ switch ( npc.GetTeam() )
+ {
+ case TEAM_IMC:
+ case TEAM_MILITIA:
+ int index = svGlobal.npcsSpawnedThisFrame_scriptManagedArray[ team ]
+ if ( GetScriptManagedEntArrayLen( index ) == 0 )
+ {
+ thread AutosquadnameAssignment_Thread( index, npc, team )
+ }
+
+ AddToScriptManagedEntArray( index, npc )
+ break
+
+ default:
+ break
+ }
+}
+
+void function AutosquadnameAssignment_Thread( int scriptManagedArrayIndex, entity npc, int team )
+{
+ WaitEndFrame() // wait for everybody to spawn this frame
+
+ array<entity> entities = GetScriptManagedEntArray( scriptManagedArrayIndex )
+ if ( entities.len() <= 1 )
+ {
+ foreach ( npc in entities )
+ {
+ RemoveFromScriptManagedEntArray( scriptManagedArrayIndex, npc )
+ }
+ return
+ }
+
+ string squadName = UniqueString( "autosquad_team_" + team )
+
+ foreach ( npc in entities )
+ {
+ RemoveFromScriptManagedEntArray( scriptManagedArrayIndex, npc )
+ if ( !IsValid( npc ) )
+ continue
+ if ( npc.kv.squadname != "" )
+ continue
+ if ( !IsAlive( npc ) )
+ continue
+ SetSquad( npc, squadName )
+ }
+ Assert( GetScriptManagedEntArrayLen( scriptManagedArrayIndex ) == 0 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn_content.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn_content.gnut
new file mode 100644
index 000000000..c6e7f9f4e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spawn_content.gnut
@@ -0,0 +1,879 @@
+untyped
+
+global const PROTOTYPE_DEFAULT_TITAN_RODEO_SLOTS = 3 // Todo: remove and set this in titan_base.set
+
+global function CommonNPCOnSpawned
+global function ShouldSpawn
+global function AiSpawnContent_Init
+global function FixupTitle
+
+struct
+{
+ array<string> pilotAntiTitanWeapons
+ int nextAntiTitanWeaponAutoAssign
+} file
+
+function AiSpawnContent_Init()
+{
+ RegisterSignal( "Stop_SimulateGrenadeThink" )
+
+ if ( IsMultiplayer() )
+ file.pilotAntiTitanWeapons = [ "mp_weapon_rocket_launcher" ]
+
+ #if DEV
+ if ( IsSingleplayer() )
+ {
+ array<string> aiSettings = GetAllowedTitanAISettings()
+ foreach ( npcSettings in aiSettings )
+ {
+ asset npcModel = Dev_GetAISettingAssetByKeyField_Global( npcSettings, "DefaultModelName" )
+ string playerSettings = expect string( Dev_GetAISettingByKeyField_Global( npcSettings, "npc_titan_player_settings" ) )
+ asset playerModel = GetPlayerSettingsAssetForClassName( playerSettings, "bodymodel" )
+
+ Assert( npcModel == playerModel, "NPC settings " + npcSettings + " has model " + npcModel + ", which does not match player model for same titan " + playerModel )
+ }
+ }
+ #endif
+}
+
+
+function CommonNPCOnSpawned( entity npc )
+{
+ npc.ai.spawnTime = Time()
+ npc.ai.spawnOrigin = npc.GetOrigin()
+
+ if ( npc.HasKey( "script_goal_radius" ) )
+ {
+ var radius = npc.kv.script_goal_radius
+ if ( radius != null && radius != "" )
+ {
+ npc.AssaultSetGoalRadius( int( radius ) )
+ npc.AssaultPoint( npc.GetOrigin() )
+ }
+ }
+
+ if ( npc.HasKey( "script_goal_height" ) )
+ {
+ var height = npc.kv.script_goal_height
+ if ( height != null && height != "" )
+ {
+ npc.AssaultSetGoalHeight( int( height ) )
+ }
+ }
+
+ if ( npc.HasKey( "script_flag_killed" ) )
+ {
+ thread SetupFlagKilledForNPC( npc )
+ }
+
+ string aisetting = GetDefaultAISetting( npc )
+
+ SetAISettingsWrapper( npc, aisetting )
+
+ Assert( !npc.executedSpawnOptions, npc + " tried to spawn twice?" )
+ npc.executedSpawnOptions = true
+
+ if ( npc.Dev_GetAISettingByKeyField( "SpawnLimping" ) )
+ npc.SetActivityModifier( ACT_MODIFIER_STAGGER, true )
+
+ InitHighlightSettings( npc )
+
+ if ( npc.Dev_GetAISettingByKeyField( "DrawTargetHealthBar" ) )
+ npc.SetValidHealthBarTarget( true )
+
+ // baseclass logic
+ if ( npc.IsTitan() )
+ {
+ if ( !SpawnWithoutSoul( npc ) )
+ {
+ CreateTitanSoul( npc )
+ }
+ }
+
+ if ( npc.GetTeam() <= 0 )
+ {
+ SetTeam( npc, expect int( npc.kv.teamnumber.tointeger() ) )
+ }
+
+ if ( IsMinion( npc ) )
+ {
+ SetupMinionForRPGs( npc )
+ CommonMinionInit( npc )
+ }
+ else if ( !npc.IsTitan() )
+ {
+ npc.SetEnemyChangeCallback( OnEnemyChanged_MinionUpdateAimSettingsForEnemy )
+ }
+
+ if ( npc.GetTitle() == "" )
+ {
+ var title = npc.GetSettingTitle()
+ if ( title != null && title != "" )
+ npc.SetTitle( title )
+ }
+
+ // start alert
+ if ( npc.mySpawnOptions_alert != null )
+ npc.kv.alwaysalert = npc.mySpawnOptions_alert
+ else if ( npc.HasKey( "start_alert") )
+ npc.kv.alwaysalert = npc.kv.start_alert
+
+ npc.kv.physdamagescale = 1.0
+
+ if ( npc.HasKey( "script_buddha" ) && npc.kv.script_buddha == "1" )
+ {
+ npc.ai.buddhaMode = true
+ }
+
+ if ( npc.IsTitan() )
+ {
+ // set boss titan type before setting proficiency for Titans
+ if ( npc.HasKey( "TitanType" ) )
+ {
+ npc.ai.bossTitanType = int( npc.kv.TitanType )
+
+ // this is to get rid of all weak titans
+ if ( npc.ai.bossTitanType == TITAN_WEAK )
+ {
+ CodeWarning( "Spawned weak Titan at " + npc.GetOrigin() + ". Change TitanType to Henchman Titan." )
+
+ // GetSettingsTitle() is causing a script error. Removed for now.
+ // CodeWarning( "Spawned weak Titan " + npc.GetSettingsTitle() + " at " + npc.GetOrigin() + ". Change TitanType to Henchman Titan." )
+ npc.ai.bossTitanType = TITAN_HENCH
+ }
+ }
+
+ if ( npc.HasKey( "disable_vdu" ) )
+ npc.ai.bossTitanVDUEnabled = int( npc.kv.disable_vdu ) == 0
+ }
+
+ // Set proficiency before giving weapons
+ SPMP_UpdateNPCProficiency( npc )
+
+ if ( npc.IsTitan() )
+ {
+ UpdateTitanMinimapStatusToOtherPlayers( npc )
+ CommonNPCTitanOnSpawned( npc )
+// Assert( npc.Dev_GetAISettingByKeyField( "footstep_type" ) != "", "NPC " + npc + " has no footstep type set" )
+ }
+ else
+ {
+ UpdateAIMinimapStatusToOtherPlayers( npc )
+
+ if ( npc.ai.mySpawnOptions_weapon != null )
+ {
+ array<entity> weapons = npc.GetMainWeapons()
+ TakeWeaponsForArray( npc, weapons )
+
+ NPCDefaultWeapon spawnoptionsweapon = expect NPCDefaultWeapon( npc.ai.mySpawnOptions_weapon )
+ npc.GiveWeapon( spawnoptionsweapon.wep, spawnoptionsweapon.mods )
+ }
+
+ entity weapon = npc.GetActiveWeapon()
+ if ( weapon != null && weapon.GetWeaponType() == WT_SIDEARM )
+ npc.DisableNPCFlag( NPC_CROUCH_COMBAT )
+ }
+
+ if ( npc.HasKey( "drop_battery" ) )
+ {
+ npc.ai.shouldDropBattery = (npc.kv.drop_battery == "1")
+ }
+
+ switch ( npc.GetClassName() )
+ {
+ case "npc_bullseye":
+ npc.NotSolid()
+ npc.SetInvulnerable()
+ break
+
+ case "npc_drone":
+ InitMinimapSettings( npc )
+
+ if ( GetMarvinType( npc ) == "marvin_type_drone" )
+ {
+ thread MarvinJobThink( npc )
+ return
+ }
+
+ npc.s.rebooting <- null
+ npc.ai.preventOwnerDamage = true
+ npc.s.lastSmokeDeployTime <- Time()
+
+ thread RunDroneTypeThink( npc )
+
+ switch ( GetDroneType( npc ) )
+ {
+ case "drone_type_engineer_combat":
+ npc.kv.rendercolor = "0 0 0"
+ break
+
+ case "drone_type_engineer_shield":
+ npc.kv.rendercolor = "255 255 255"
+ break
+ }
+ break
+
+ case "npc_dropship":
+ npc.SetSkin( 1 ) //Use skin where the lights are on for dropship.
+ npc.EnableRenderAlways()
+ npc.SetAimAssistAllowed( false )
+ //npc.kv.CollisionGroup = TRACE_COLLISION_GROUP_BLOCK_WEAPONS
+ AddAnimEvent( npc, "dropship_warpout", WarpoutEffect )
+
+ InitLeanDropship( npc )
+ break
+
+
+ case "npc_frag_drone":
+ MakeSuicideSpectre( npc )
+ break
+
+ case "npc_gunship":
+ InitMinimapSettings( npc )
+ EmitSoundOnEntity( npc, SOUND_GUNSHIP_HOVER )
+
+ npc.ai.preventOwnerDamage = true
+ npc.s.rebooting <- null
+ npc.s.plantedMinesManagedEntArrayID <- CreateScriptManagedEntArray()
+
+ npc.kv.crashOnDeath = false
+ //npc.kv.secondaryWeaponName = "mp_weapon_gunship_missile"
+
+ EnableLeeching( npc )
+ npc.SetUsableByGroup( "enemies pilot" )
+
+ thread GunshipThink( npc )
+ break
+
+ case "npc_marvin":
+ asset model = npc.GetModelName()
+ npc.EnableNPCFlag( NPC_DISABLE_SENSING ) // don't do traces to look for enemies or players
+ thread MarvinFace( npc )
+ thread MarvinJobThink( npc )
+ break
+
+ case "npc_pilot_elite":
+ npc.kv.physdamagescale = 1.0
+ npc.kv.WeaponProficiency = eWeaponProficiency.VERYGOOD
+ break
+
+ case "npc_prowler":
+ npc.kv.disengageEnemyDist = 1500
+ npc.DisableNPCFlag( NPC_ALLOW_FLEE ) //HACK until we get a way to make last guy not run away and hide
+ //SetSquad( npc, spawnOptions.squadName ) //not sure why this is here - jake had it in his original spawn func, so I'm keeping it
+ //SetNPCSquadMode( spawnOptions.squadName, SQUAD_MODE_MULTIPRONGED_ATTACK )
+ break
+
+ case "npc_soldier":
+
+ InitMinimapSettings( npc )
+
+ SetHumanRagdollImpactTable( npc )
+
+ npc.EnableNPCFlag( NPC_CROUCH_COMBAT )
+
+ thread OnSoldierSeeEnemy( npc )
+ thread TryFriendlyPassingNearby( npc )
+
+ int team = npc.GetTeam()
+
+ //grunt specific
+ npc.SetDoFaceAnimations( true ) //HACK: assumption that militia are the only grunt models with faces ( will need a better thing for R2 )
+
+ bool alreadyGaveASecondary = false;
+ entity weapon = npc.GetActiveWeapon()
+ string weaponSubClass
+ if ( weapon )
+ weaponSubClass = string( weapon.GetWeaponInfoFileKeyField( "weaponSubClass" ) )
+
+ #if SP
+ if ( weaponSubClass == "sniper" )
+ {
+ if ( AssignDefaultNPCSidearm( npc ) )
+ alreadyGaveASecondary = true
+ }
+ #endif
+
+ if ( !alreadyGaveASecondary && SP_GetPilotAntiTitanWeapon( npc ) == null )
+ TryAutoAssignAntiTitanWeapon( npc )
+
+ if ( npc.Dev_GetAISettingByKeyField( "PersonalShield" ) != null )
+ {
+ npc.DisableNPCFlag( NPC_ALLOW_FLEE | NPC_ALLOW_HAND_SIGNALS | NPC_USE_SHOOTING_COVER | NPC_CROUCH_COMBAT )
+ thread ActivatePersonalShield( npc )
+ }
+
+ if ( npc.ai.droneSpawnAISettings != "" )
+ {
+ thread DroneGruntThink( npc, npc.ai.droneSpawnAISettings )
+ }
+
+ AssignGruntModelForWeaponClass( npc, weapon, weaponSubClass )
+
+ break
+
+ case "npc_spectre":
+ InitMinimapSettings( npc )
+ thread OnSpectreSeeEnemy( npc )
+
+ if ( IsMultiplayer() )
+ {
+ npc.EnableNPCFlag( NPC_CROUCH_COMBAT )
+ //Only enable spectre hacking if the playlist var is enabled
+ if ( ( npc.GetTeam() == TEAM_IMC || npc.GetTeam() == TEAM_MILITIA ) && GetCurrentPlaylistVarInt( "enable_spectre_hacking", 0 ) == 1 )
+ {
+ EnableLeeching( npc )
+ npc.SetUsableByGroup( "enemies pilot" )
+ }
+ }
+ else
+ {
+ EnableLeeching( npc )
+ npc.SetUsableByGroup( "enemies pilot" )
+
+ if ( npc.HasKey( "carrying_battery" ) )
+ {
+ if ( npc.kv.carrying_battery == "1" )
+ {
+ thread NPCCarriesBattery( npc )
+ }
+ }
+ }
+
+ if ( SP_GetPilotAntiTitanWeapon( npc ) == null )
+ TryAutoAssignAntiTitanWeapon( npc )
+ break
+
+ case "npc_stalker":
+ InitMinimapSettings( npc )
+
+ if ( IsSingleplayer() && npc.kv.squadname != "" )
+ SetNPCSquadMode( npc.kv.squadname, SQUAD_MODE_MULTIPRONGED_ATTACK )
+
+ break
+
+ case "npc_super_spectre":
+
+ InitMinimapSettings( npc )
+
+ npc.GiveOffhandWeapon( "mp_weapon_spectre_spawner", 0 )
+
+ DisableLeeching( npc )
+
+ npc.SetCapabilityFlag( bits_CAP_NO_HIT_SQUADMATES, false )
+
+ npc.ai.preventOwnerDamage = true
+
+ npc.SetDeathNotifications( true )
+
+ AddAnimEvent( npc, "SuperSpectre_OnGroundSlamImpact", SuperSpectre_OnGroundSlamImpact )
+ AddAnimEvent( npc, "SuperSpectre_OnGroundLandImpact", SuperSpectre_OnGroundLandImpact )
+
+ thread SuperSpectreThink( npc )
+
+ SuperSpectreIntro( npc )
+ break
+
+
+ case "npc_titan":
+ InitMinimapSettings( npc )
+
+ // used so the titan can stand/kneel without cutting off functionality
+ npc.s.standQueued <- false
+ npc.ai.preventOwnerDamage = true
+ if ( IsMultiplayer() )
+ {
+ npc.e.hasDefaultEnemyHighlight = true
+ SetDefaultMPEnemyHighlight( npc )
+ }
+ break
+
+
+ case "npc_turret_mega":
+ InitMinimapSettings( npc )
+ npc.EnableNPCFlag( NPC_AIM_DIRECT_AT_ENEMY )
+ npc.SetAimAssistAllowed( false )
+ #if R1_VGUI_MINIMAP
+ npc.Minimap_SetDefaultMaterial( GetMinimapMaterial( "turret_neutral" ) )
+ npc.Minimap_SetFriendlyMaterial( GetMinimapMaterial( "turret_friendly" ) )
+ npc.Minimap_SetEnemyMaterial( GetMinimapMaterial( "turret_enemy" ) )
+ npc.Minimap_SetBossPlayerMaterial( GetMinimapMaterial( "turret_friendly" ) )
+ #endif
+ break
+
+ case "npc_turret_sentry":
+ InitMinimapSettings( npc )
+ npc.SetAimAssistAllowed( false )
+ break
+
+ }
+
+ thread AssaultLinkedMoveTarget( npc )
+
+ FixupTitle( npc )
+ #if DEV
+ // stop all the wandering in sp_enemies.
+ if ( GetMapName() == "sp_enemies" )
+ npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+ #endif
+}
+
+void function FixupTitle( entity npc )
+{
+ if ( IsMultiplayer() )
+ return
+
+ if ( !npc.IsTitan() )
+ npc.SetTitle( "" )
+ /*
+ if ( npc.GetTitle() == "" )
+ return
+ switch ( npc.GetTeam() )
+ {
+ case TEAM_UNASSIGNED:
+ case TEAM_MILITIA:
+ break
+ default:
+ npc.SetTitle( "" )
+ break
+ }
+ */
+}
+
+var function GetTitanHotdropSetting( entity npc )
+{
+ if ( npc.mySpawnOptions_titanfallSpawn != null )
+ return "titanfall"
+ if ( npc.mySpawnOptions_warpfallSpawn != null )
+ return "warpfall"
+
+ if ( npc.HasKey( "script_hotdrop" ) )
+ {
+ switch ( npc.kv.script_hotdrop )
+ {
+ case "0":
+ return null
+ case "3":
+ case "1":
+ return "titanfall"
+ case "4":
+ case "2":
+ return "warpfall"
+ }
+ }
+
+ return null
+}
+
+function CommonNPCTitanOnSpawned( entity npc )
+{
+ if ( npc.ai.titanSpawnLoadout.primary != "" )
+ {
+ Assert( npc.GetMainWeapons().len() == 0 )
+
+ // if designer overwrites weapons, apply them
+ GiveTitanLoadout( npc, npc.ai.titanSpawnLoadout )
+ }
+ else if ( npc.GetMainWeapons().len() == 0 )
+ {
+ GiveTitanLoadout( npc, npc.ai.titanSpawnLoadout )
+ }
+
+ //Assert( npc.ai.titanSpawnLoadout.setFile == npc.ai.titanSettings.titanSetFile )
+ string playerSettings = expect string( npc.Dev_GetAISettingByKeyField( "npc_titan_player_settings" ) )
+ asset modelName = GetPlayerSettingsAssetForClassName( playerSettings, "bodymodel" )
+ if ( npc.GetModelName() != modelName )
+ npc.SetModel( modelName )
+// Assert( npc.GetModelName() == modelName )
+
+ int camoIndex = GetTitanCamoIndexFromLoadoutAndPrimeStatus( npc.ai.titanSpawnLoadout )
+ int skinIndex = GetTitanSkinIndexFromLoadoutAndPrimeStatus( npc.ai.titanSpawnLoadout )
+ int decalIndex = GetTitanDecalIndexFromLoadoutAndPrimeStatus ( npc.ai.titanSpawnLoadout )
+
+ if ( camoIndex > 0 )
+ {
+ npc.SetSkin( TITAN_SKIN_INDEX_CAMO )
+ npc.SetCamo( camoIndex )
+ }
+ else
+ {
+ int skin
+ if ( npc.HasKey( "modelskin" ) )
+ skin = expect int( npc.kv.modelskin.tointeger() )
+
+ if ( skinIndex > 0 )
+ {
+ Assert( skin == 0, "Both npc.kv.modelskin and skinIndex were > 0. Pick one." )
+ skin = skinIndex
+ }
+
+ if ( skin > 0 )
+ npc.SetSkin( skin )
+ }
+
+ npc.SetDecal( decalIndex )
+
+ #if HAS_BOSS_AI
+ if ( IsMercTitan( npc ) )
+ {
+ array<entity> weapons = GetPrimaryWeapons( npc )
+ Assert( weapons.len() == 1 )
+ string character = GetMercCharacterForWeapon( weapons[0].GetWeaponClassName() )
+ npc.ai.bossCharacterName = character
+ npc.ai.mercCharacterID = GetBossTitanID( character )
+
+ int id = GetBossTitanID( character )
+ string title = GetBossTitleFromID( id )
+
+ npc.SetTitle( title )
+ }
+ #endif
+
+ // force sp titans to use specific loadouts
+ if ( !IsMultiplayer() )
+ ResetTitanLoadoutFromPrimary( npc )
+
+ npc.EnableNPCFlag( NPC_NO_MOVING_PLATFORM_DEATH )
+ //npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+
+ if ( IsMultiplayer() )
+ {
+ npc.kv.alwaysalert = 1
+ }
+ else
+ {
+ if ( npc.GetAIClass() == AIC_TITAN_BUDDY )
+ {
+ npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE )
+ array<entity> enemies = GetNPCArrayEx( "any", TEAM_ANY, npc.GetTeam(), npc.GetOrigin(), 4000 )
+ if ( enemies.len() > 0 )
+ npc.SetAlert()
+
+ if ( npc.GetTitanSoul() )
+ {
+ // create buddy titan dialogue ent
+ // it will be transfered during embark and disembark automatically
+ entity dialogueEnt = CreateScriptMover()
+ dialogueEnt.DisableHibernation()
+ dialogueEnt.SetParent( npc, "HEADFOCUS", false, 0 )
+ npc.GetTitanSoul().SetTitanSoulNetEnt( "dialogueEnt", dialogueEnt )
+ }
+ }
+ }
+
+ local maxHealth = GetPlayerSettingsFieldForClassName_Health( npc.ai.titanSettings.titanSetFile ) //FD META - TO UPDATE with NPC equivalent of .GetPlayerModHealth()
+ if ( npc.ai.titanSpawnLoadout.setFileMods.contains( "fd_health_upgrade" ) )
+ maxHealth += 2500
+ //TEMP - GetPlayerSettingsFieldForClassName_Health doesn't return modded values.
+ if ( IsHardcoreGameMode() )
+ maxHealth *= 0.5
+
+ // this will override whatever health is set in the aisettings txt file.
+ npc.SetMaxHealth( maxHealth )
+ npc.SetHealth( maxHealth )
+ npc.SetValidHealthBarTarget( true )
+
+ #if HAS_BOSS_AI
+ UpdateMercTitanHealthForDifficulty( npc )
+ #endif
+
+ switch ( GetTitanHotdropSetting( npc ) )
+ {
+ case "titanfall":
+ thread NPCTitanHotdrops( npc, true )
+ break
+
+ case "warpfall":
+ thread NPCTitanHotdrops( npc, true, "at_hotdrop_drop_2knee_turbo_upgraded" )
+ break
+ }
+
+ // TODO: Have code allow us to put this in titan_base.set
+ npc.SetNumRodeoSlots( PROTOTYPE_DEFAULT_TITAN_RODEO_SLOTS )
+
+ if ( IsValid( npc.mySpawnOptions_ownerPlayer ) )
+ {
+ entity soul = npc.GetTitanSoul()
+ entity player = expect entity( npc.mySpawnOptions_ownerPlayer )
+
+ if ( IsValid( soul ) )
+ {
+ soul.soul.lastOwner = player
+ SoulBecomesOwnedByPlayer( soul, player )
+ }
+
+ SetupAutoTitan( npc, player )
+ }
+
+ if ( npc.HasKey( "disable_offhand_ordnance" ) )
+ {
+ if ( bool( npc.kv.disable_offhand_ordnance ) )
+ {
+ npc.TakeOffhandWeapon( OFFHAND_ORDNANCE )
+ }
+ }
+
+ if ( npc.HasKey( "disable_offhand_defense" ) )
+ {
+ if ( bool( npc.kv.disable_offhand_defense ) )
+ {
+ npc.TakeOffhandWeapon( OFFHAND_SPECIAL )
+ }
+ }
+
+ if ( npc.HasKey( "disable_offhand_tactical" ) )
+ {
+ if ( bool( npc.kv.disable_offhand_tactical ) )
+ {
+ entity weapon = npc.GetOffhandWeapon( OFFHAND_ANTIRODEO )
+ if ( weapon && weapon.GetWeaponClassName() == "mp_titanability_hover" )
+ npc.SetAllowSpecialJump( false )
+
+ npc.TakeOffhandWeapon( OFFHAND_ANTIRODEO )
+ }
+ }
+
+ if ( npc.HasKey( "disable_offhand_core" ) )
+ {
+ if ( bool( npc.kv.disable_offhand_core ) )
+ {
+ npc.TakeOffhandWeapon( OFFHAND_EQUIPMENT )
+ }
+ }
+
+ if ( npc.HasKey( "follow_mode" ) )
+ {
+ if ( bool( npc.kv.follow_mode ) )
+ {
+ entity player = GetPlayerArray()[0] // gross
+ int followBehavior = GetDefaultNPCFollowBehavior( npc )
+ npc.InitFollowBehavior( player, followBehavior )
+ npc.EnableBehavior( "Follow" )
+ npc.DisableBehavior( "Assault" )
+ }
+ }
+
+ var hasTraverse = npc.Dev_GetAISettingByKeyField( "can_traverse" )
+ if ( hasTraverse == null || expect int( hasTraverse ) == 0 )
+ {
+ npc.SetCapabilityFlag( bits_CAP_MOVE_TRAVERSE, false )
+ }
+
+ entity soul = npc.GetTitanSoul()
+ if ( IsValid( soul ) )
+ {
+ soul.soul.titanLoadout = npc.ai.titanSpawnLoadout
+ }
+}
+
+function ShouldSpawn( team, forced )
+{
+ //we're not allowed to spawn AI at all - return false
+ if ( !IsNPCSpawningEnabled() && !forced )
+ {
+ printt( "WARNING: tried to spawn an NPC but NPC Spawning is Disabled." )
+ return false
+ }
+ return true
+}
+
+
+function HACK_DroneGruntModel( grunt )
+{
+ string tag = "CHESTFOCUS"
+ int attachID = expect int( grunt.LookupAttachment( tag ) )
+ vector origin = expect vector( grunt.GetAttachmentOrigin( attachID ) )
+ vector angles = expect vector( grunt.GetAttachmentAngles( attachID ) )
+ vector forward = AnglesToForward( angles )
+ vector right = AnglesToRight( angles )
+ vector up = AnglesToUp( angles )
+
+ vector angles1 = AnglesCompose( angles, Vector( 0, -90, 90 ) )
+ vector origin1 = origin + ( forward * -4 ) + ( up * -1.5 )
+ entity back1 = CreatePropDynamic( HACK_DRONE_BACK1, origin1, angles1 )
+ back1.SetParent( grunt, tag, true, 0 )
+
+ vector angles2 = AnglesCompose( angles, Vector( 0, -90, 0 ) )
+ vector origin2 = origin + ( forward * -9 ) + ( up * 11 ) + ( right * -1 )
+ entity back2 = CreatePropDynamic( HACK_DRONE_BACK2, origin2, angles2 )
+ back2.SetParent( grunt, tag, true, 0 )
+}
+
+void function TryAutoAssignAntiTitanWeapon( entity npc )
+{
+ // disabling this while anti titan weapons settle down
+ if ( !IsMultiplayer() )
+ return
+
+ if ( file.pilotAntiTitanWeapons.len() == 0 )
+ return
+
+ Assert( !HasAntiTitanWeapon( npc ) )
+
+ // each 4th npc gets a rocket
+ file.nextAntiTitanWeaponAutoAssign--
+ if ( file.nextAntiTitanWeaponAutoAssign > 0 )
+ return
+
+ file.nextAntiTitanWeaponAutoAssign = 3
+
+ string weapon = file.pilotAntiTitanWeapons.getrandom()
+ npc.GiveWeapon( weapon )
+
+ if ( IsGrunt( npc ) )
+ {
+ // show rockets on the back
+ switch ( npc.GetTeam() )
+ {
+ case TEAM_IMC:
+ npc.SetModel( TEAM_IMC_GRUNT_MODEL_ROCKET )
+ break
+
+#if SP
+ case TEAM_MILITIA:
+ npc.SetModel( TEAM_MIL_GRUNT_MODEL_ROCKET )
+ break
+#endif
+ }
+ }
+}
+
+function SpawnWithoutSoul( ent )
+{
+ if ( ent.HasKey( "noSoul" ) )
+ {
+ return ent.kv.noSoul
+ }
+
+ return "spawnWithoutSoul" in ent.s
+}
+
+function DisableAimAssisst( self )
+{
+ self.SetAimAssistAllowed( false )
+}
+
+void function SuperSpectreIntro( entity npc )
+{
+ bool warpfall
+ if ( npc.mySpawnOptions_warpfallSpawn != null )
+ warpfall = true
+ else if ( npc.HasKey( "script_hotdrop" ) && npc.kv.script_hotdrop.tolower() == "warpfall" )
+ warpfall = true
+
+ if ( warpfall )
+ thread SuperSpectre_WarpFall( npc )
+}
+
+void function AssignGruntModelForWeaponClass( entity npc, entity weapon, string weaponSubClass )
+{
+ // We only have IMC grunt models for weapon class
+ if ( !npc.Dev_GetAISettingByKeyField( "IsGenericGrunt" ) )
+ return
+
+ asset model
+
+ switch ( npc.GetTeam() )
+ {
+//#if SP
+ case TEAM_MILITIA:
+ switch ( weaponSubClass )
+ {
+ case "lmg":
+ case "sniper":
+ model = TEAM_MIL_GRUNT_MODEL_LMG
+ break
+
+ case "rocket":
+ case "shotgun":
+ case "projectile_shotgun":
+ model = TEAM_MIL_GRUNT_MODEL_SHOTGUN
+ break
+
+ case "handgun":
+ case "smg":
+ case "sidearm":
+ model = TEAM_MIL_GRUNT_MODEL_SMG
+ break
+
+ case "rifle":
+ default:
+ model = TEAM_MIL_GRUNT_MODEL_RIFLE
+ break
+ }
+ break
+//#endif
+
+ case TEAM_IMC:
+ default:
+ switch ( weaponSubClass )
+ {
+ case "lmg":
+ case "sniper":
+ model = TEAM_IMC_GRUNT_MODEL_LMG
+ break
+
+ case "rocket":
+ case "shotgun":
+ case "projectile_shotgun":
+ model = TEAM_IMC_GRUNT_MODEL_SHOTGUN
+ break
+
+ case "handgun":
+ case "smg":
+ case "sidearm":
+ model = TEAM_IMC_GRUNT_MODEL_SMG
+ break
+
+ case "rifle":
+ default:
+#if SP
+ model = TEAM_IMC_GRUNT_MODEL_RIFLE
+#else
+ // no shotgun/smg grunts in MP right now
+ switch ( RandomInt( 3 ) )
+ {
+ case 0:
+ model = TEAM_IMC_GRUNT_MODEL_RIFLE
+ break
+ case 1:
+ model = TEAM_IMC_GRUNT_MODEL_SHOTGUN
+ break
+ case 2:
+ model = TEAM_IMC_GRUNT_MODEL_SMG
+ break
+ }
+#endif
+ break
+ }
+ break
+
+ }
+
+ if ( model != $"" )
+ {
+ npc.SetModel( model )
+ return
+ }
+
+ if ( IsValid( weapon ) )
+ CodeWarning( "Grunt at " + npc.GetOrigin() + " couldnt get assigned a body model for weapon " + weapon.GetWeaponClassName() + " because that weapon is missing or has invalid weaponSubClass field" )
+ else
+ CodeWarning( "Grunt at " + npc.GetOrigin() + " has no weapon" )
+}
+
+
+entity function SP_GetPilotAntiTitanWeapon( entity ent )
+{
+ array<entity> weaponsArray = ent.GetMainWeapons()
+ foreach ( weapon in weaponsArray )
+ {
+ foreach ( weaponName in file.pilotAntiTitanWeapons )
+ {
+ if ( weapon.GetWeaponClassName() == weaponName )
+ return weapon
+ }
+ }
+
+ return null
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spectre.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spectre.gnut
new file mode 100644
index 000000000..214aff96e
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_spectre.gnut
@@ -0,0 +1,131 @@
+global function AiSpectre_Init
+global function NPCCarriesBattery
+
+void function AiSpectre_Init()
+{
+ //AddDamageCallback( "npc_spectre", SpectreOnDamaged )
+ AddDeathCallback( "npc_spectre", SpectreOnDeath )
+ //AddSpawnCallback( "npc_spectre", SpectreOnSpawned )
+
+ #if !SPECTRE_CHATTER_MP_ENABLED
+ AddCallback_OnPlayerKilled( SpectreChatter_OnPlayerKilled )
+ AddCallback_OnNPCKilled( SpectreChatter_OnNPCKilled )
+ #endif
+}
+
+void function SpectreOnSpawned( entity npc )
+{
+
+}
+
+void function SpectreOnDeath( entity npc, var damageInfo )
+{
+ if ( !IsValidHeadShot( damageInfo, npc ) )
+ return
+
+ // Set these so cl_player knows to kill the eye glow and play the right SFX
+ DamageInfo_AddCustomDamageType( damageInfo, DF_HEADSHOT )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_KILLSHOT )
+// EmitSoundOnEntityExceptToPlayer( npc, attacker, "SuicideSpectre.BulletImpact_HeadShot_3P_vs_3P" )
+
+ int bodyGroupIndex = npc.FindBodyGroup( "removableHead" )
+ int stateIndex = 1 // 0 = show, 1 = hide
+ npc.SetBodygroup( bodyGroupIndex, stateIndex )
+
+ DamageInfo_SetDamage( damageInfo, npc.GetMaxHealth() )
+
+}
+
+// All damage to spectres comes here for modification and then either branches out to other npc types (Suicide, etc) for custom stuff or it just continues like normal.
+void function SpectreOnDamaged( entity npc, var damageInfo )
+{
+
+}
+
+void function SpectreChatter_OnPlayerKilled( entity playerKilled, entity attacker, var damageInfo )
+{
+ if ( !IsSpectre( attacker ) )
+ return
+
+ if ( playerKilled.IsTitan() )
+ thread PlaySpectreChatterAfterDelay( attacker, "spectre_gs_gruntkillstitan_02_1" )
+ else
+ thread PlaySpectreChatterAfterDelay( attacker, "spectre_gs_killenemypilot_01_1" )
+
+}
+
+void function SpectreChatter_OnNPCKilled( entity npcKilled, entity attacker, var damageInfo )
+{
+ if ( IsSpectre( npcKilled ) )
+ {
+ string deadGuySquadName = expect string( npcKilled.kv.squadname )
+ if ( deadGuySquadName == "" )
+ return
+
+ array<entity> squad = GetNPCArrayBySquad( deadGuySquadName )
+
+ entity speakingSquadMate = null
+
+ foreach( squadMate in squad )
+ {
+ if ( IsSpectre( squadMate ) )
+ {
+ speakingSquadMate = squadMate
+ break
+ }
+ }
+ if ( speakingSquadMate == null )
+ return
+
+ if ( squad.len() == 1 )
+ thread PlaySpectreChatterAfterDelay( speakingSquadMate, "spectre_gs_squaddeplete_01_1" )
+ else if ( squad.len() > 0 )
+ thread PlaySpectreChatterAfterDelay( speakingSquadMate, "spectre_gs_allygrundown_05_1" )
+ }
+ else
+ {
+ if ( !IsSpectre( attacker ) )
+ return
+
+ if ( npcKilled.IsTitan() )
+ thread PlaySpectreChatterAfterDelay( attacker, "spectre_gs_gruntkillstitan_02_1" )
+ }
+}
+
+void function PlaySpectreChatterAfterDelay( entity spectre, string chatterLine, float delay = 0.3 )
+{
+ wait delay
+
+ if ( !IsAlive( spectre ) ) //Really this is just an optimization thing, if the spectre is dead no point in running the same check for every player nearby in ShouldPlaySpectreChatterMPLine
+ return
+
+ PlaySpectreChatterToAll( chatterLine, spectre )
+}
+
+void function NPCCarriesBattery( entity npc )
+{
+ entity battery = Rodeo_CreateBatteryPack()
+ battery.SetParent( npc, "BATTERY_ATTACH" )
+ battery.MarkAsNonMovingAttachment()
+ thread SpectreBatteryThink( npc, battery )
+}
+
+void function SpectreBatteryThink( entity npc, entity battery )
+{
+ battery.EndSignal( "OnDestroy" )
+ npc.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( battery )
+ {
+ if ( IsValid( battery ) )
+ {
+ battery.ClearParent()
+ battery.SetAngles( < 0,0,0 > )
+ battery.SetVelocity( < 0,0,200 > )
+ }
+ }
+ )
+
+ npc.WaitSignal( "OnDeath" )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut
new file mode 100644
index 000000000..f49560e02
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut
@@ -0,0 +1,606 @@
+global function AiStalker_Init
+global function GetDeathForce
+global function StalkerGearOverloads
+global function StalkerMeltingDown
+
+global function IsStalkerLimbBlownOff
+
+const float STALKER_DAMAGE_REQUIRED_TO_HEADSHOT = 0.3
+//
+// Base npc script shared between all npc types (regular, suicide, etc.)
+//
+
+const STALKER_REACTOR_CRITIMPACT_SOUND_1P_VS_3P = "ai_stalker_bulletimpact_nukecrit_1p_vs_3p"
+const STALKER_REACTOR_CRITIMPACT_SOUND_3P_VS_3P = "ai_stalker_bulletimpact_nukecrit_3p_vs_3p"
+const STALKER_REACTOR_CRITICAL_SOUND = "ai_stalker_nukedestruct_warmup_3p"
+const STALKER_REACTOR_CRITICAL_FX = $"P_spectre_suicide_warn"
+
+void function AiStalker_Init()
+{
+ PrecacheImpactEffectTable( "exp_stalker_powersupply" )
+ PrecacheImpactEffectTable( "exp_small_stalker_powersupply" )
+ PrecacheParticleSystem( STALKER_REACTOR_CRITICAL_FX )
+ AddDamageCallback( "npc_stalker", StalkerOnDamaged )
+ AddDeathCallback( "npc_stalker", StalkerOnDeath )
+ AddSpawnCallback( "npc_stalker", StalkerOnSpawned )
+}
+
+void function StalkerOnSpawned( entity npc )
+{
+ StalkerOnSpawned_Think( npc )
+}
+
+void function StalkerOnSpawned_Think( entity npc )
+{
+ npc.SetCanBeMeleeExecuted( false )
+
+ for ( int hitGroup = 0; hitGroup < HITGROUP_COUNT; hitGroup++ )
+ {
+ npc.ai.stalkerHitgroupDamageAccumulated[ hitGroup ] <- 0
+ npc.ai.stalkerHitgroupLastHitTime[ hitGroup ] <- 0
+ }
+
+ if ( npc.Dev_GetAISettingByKeyField( "ScriptSpawnAsCrawler" ) == 1 )
+ {
+ EnableStalkerCrawlingBehavior( npc )
+ PlayCrawlingAnim( npc, "ACT_RUN" )
+ npc.Anim_Stop() // start playing a crawl anim then cut it off so it doesnt loop
+ }
+}
+
+void function StalkerOnDeath( entity npc, var damageInfo )
+{
+ thread StalkerOnDeath_Internal( npc, damageInfo )
+
+ #if MP
+ int sourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ if ( sourceId == eDamageSourceId.damagedef_titan_step )
+ {
+ Explosion_DamageDefSimple(
+ damagedef_stalker_powersupply_explosion_large_at,
+ npc.GetOrigin(),
+ npc,
+ npc,
+ npc.GetOrigin()
+ )
+ }
+ #endif
+
+}
+
+void function StalkerOnDeath_Internal( entity npc, var damageInfo )
+{
+ int customDamageFlags = DamageInfo_GetCustomDamageType( damageInfo )
+ bool allowDismemberment = bool( customDamageFlags & DF_DISMEMBERMENT )
+ if ( allowDismemberment )
+ {
+ int hitGroup = GetHitGroupFromDamageInfo( npc, damageInfo )
+ if ( hitGroup >= HITGROUP_GENERIC )
+ {
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ TryDismemberStalker( npc, damageInfo, attacker, hitGroup )
+ }
+ }
+
+ if ( IsCrawling( npc ) )
+ {
+ WaitFrame() // or head won't disappear
+ if ( IsValid( npc ) )
+ npc.BecomeRagdoll( Vector( 0, 0, 0 ), false )
+ return
+ }
+}
+
+
+// All damage to stalkers comes here for modification and then either branches out to other npc types (Suicide, etc) for custom stuff or it just continues like normal.
+void function StalkerOnDamaged( entity npc, var damageInfo )
+{
+ StalkerOnDamaged_Internal( npc, damageInfo )
+}
+
+void function StalkerOnDamaged_Internal( entity npc, var damageInfo )
+{
+ if ( !IsAlive( npc ) )
+ return
+
+ if ( StalkerMeltingDown( npc ) )
+ {
+ DamageInfo_ScaleDamage( damageInfo, 0.0 )
+ return
+ }
+
+ // can't shoot, don't blow off limbs
+ if ( IsCrawling( npc ) )
+ {
+ if ( Time() - npc.ai.startCrawlingTime < 0.75 )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ }
+
+ int hitGroup = GetHitGroupFromDamageInfo( npc, damageInfo )
+ if ( hitGroup < HITGROUP_GENERIC )
+ hitGroup = HITGROUP_GENERIC
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ // limb dead yet?
+ npc.ai.stalkerHitgroupDamageAccumulated[ hitGroup ] += int( damage )
+ npc.ai.stalkerHitgroupLastHitTime[ hitGroup ] = Time()
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( PlayerHitGear( npc, damageInfo, hitGroup ) )
+ {
+ // don't die from damage
+ float damage = DamageInfo_GetDamage( damageInfo )
+ damage = npc.GetHealth() - 1.0
+ DamageInfo_SetDamage( damageInfo, damage )
+
+ thread StalkerGearOverloads( npc, attacker )
+ return
+ }
+
+ int customDamageFlags = DamageInfo_GetCustomDamageType( damageInfo )
+ bool allowDismemberment = bool( customDamageFlags & DF_DISMEMBERMENT )
+ if ( !allowDismemberment )
+ return
+
+ bool canBeStaggered = TryDismemberStalker( npc, damageInfo, attacker, hitGroup )
+
+ if ( canBeStaggered && !IsCrawling( npc ) && !npc.ai.transitioningToCrawl )
+ {
+ if ( npc.GetHealth().tofloat() / npc.GetMaxHealth().tofloat() <= 0.5 )
+ {
+ thread AttemptStandToStaggerAnimation( npc )
+ npc.SetActivityModifier( ACT_MODIFIER_STAGGER, true )
+ }
+ }
+}
+
+bool function TryDismemberStalker( entity npc, var damageInfo, entity attacker, int hitGroup )
+{
+ string fpSound
+ string tpSound
+
+ switch ( hitGroup )
+ {
+ case HITGROUP_CHEST:
+ case HITGROUP_STOMACH:
+ fpSound = "AndroidArmored.BulletImpact_1P_vs_3P"
+ tpSound = "AndroidArmored.BulletImpact_3P_vs_3P"
+ break
+
+ default:
+ fpSound = "AndroidVulnerable.BulletImpact_1P_vs_3P"
+ tpSound = "AndroidVulnerable.BulletImpact_3P_vs_3P"
+ break
+ }
+
+ if ( IsAlive( attacker ) && attacker.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( npc, attacker, fpSound )
+ EmitSoundOnEntityExceptToPlayer( npc, attacker, tpSound )
+ }
+ else
+ {
+ EmitSoundOnEntity( npc, tpSound )
+ }
+
+ bool justAFleshWound = true
+
+ switch ( hitGroup )
+ {
+ case HITGROUP_HEAD:
+ thread StalkerHeadShot( npc, damageInfo, hitGroup )
+ justAFleshWound = false
+ break
+
+ case HITGROUP_LEFTARM:
+ if ( StalkerLimbBlownOff( npc, damageInfo, hitGroup, 0.085, "left_arm", [ "left_arm", "l_hand" ], "Spectre.Arm.Explode" ) )
+ {
+ npc.SetActivityModifier( ACT_MODIFIER_ONEHANDED, true )
+
+ // Some of his synced melees depend on using his left arm
+ npc.SetCapabilityFlag( bits_CAP_SYNCED_MELEE_ATTACK, false )
+ }
+ break
+
+ case HITGROUP_LEFTLEG:
+ justAFleshWound = TryLegBlownOff( npc, damageInfo, hitGroup, 0.17, "left_leg", [ "left_leg", "foot_L_sole" ], "Spectre.Leg.Explode" )
+ break
+
+ case HITGROUP_RIGHTLEG:
+ justAFleshWound = TryLegBlownOff( npc, damageInfo, hitGroup, 0.17, "right_leg", [ "right_leg", "foot_R_sole" ], "Spectre.Leg.Explode" )
+ break
+ }
+
+ return justAFleshWound
+}
+
+bool function PlayerHitGear( entity npc, var damageInfo, int hitGroup )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !attacker.IsPlayer() )
+ return false
+
+ if ( hitGroup != HITGROUP_GEAR )
+ return false
+
+ if ( !( DamageInfo_GetCustomDamageType( damageInfo ) & DF_BULLET ) )
+ return false
+
+ return true
+}
+
+int function GetHitGroupFromDamageInfo( entity npc, var damageInfo )
+{
+ int hitGroup = DamageInfo_GetHitGroup( damageInfo )
+
+ if ( hitGroup <= HITGROUP_GENERIC )
+ {
+ int hitBox = DamageInfo_GetHitBox( damageInfo )
+ if ( hitBox >= 0 )
+ return GetHitgroupForHitboxOnEntity( npc, hitBox )
+ }
+
+ return hitGroup
+}
+
+bool function StalkerMeltingDown( entity npc )
+{
+ int bodyGroup = npc.FindBodyGroup( "gear" )
+ Assert( bodyGroup != -1 )
+
+ // gear already blown up?
+ return npc.GetBodyGroupState( bodyGroup ) != 0
+}
+
+void function StalkerGearOverloads( entity npc, entity attacker = null )
+{
+ Assert( !StalkerMeltingDown( npc ) )
+
+ if ( !IsCrawling( npc ) && StalkerCanCrawl( npc ) )
+ thread FallAndBecomeCrawlingStalker( npc )
+
+ int bodyGroup = npc.FindBodyGroup( "gear" )
+
+ // hide gear
+ npc.SetBodygroup( bodyGroup, 1 )
+
+ string attachment = "CHESTFOCUS"
+
+ npc.EndSignal( "OnDestroy" )
+ npc.EndSignal( "OnDeath" )
+
+ entity nukeFXInfoTarget = CreateEntity( "info_target" )
+ nukeFXInfoTarget.kv.spawnflags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT
+ DispatchSpawn( nukeFXInfoTarget )
+
+ nukeFXInfoTarget.SetParent( npc, attachment )
+
+ if ( attacker != null )
+ {
+ EmitSoundOnEntityOnlyToPlayer( nukeFXInfoTarget, attacker, STALKER_REACTOR_CRITIMPACT_SOUND_1P_VS_3P )
+ EmitSoundOnEntityExceptToPlayer( nukeFXInfoTarget, attacker, STALKER_REACTOR_CRITIMPACT_SOUND_3P_VS_3P )
+ }
+ else
+ {
+ EmitSoundOnEntity( nukeFXInfoTarget, STALKER_REACTOR_CRITIMPACT_SOUND_3P_VS_3P )
+ }
+
+ EmitSoundOnEntity( nukeFXInfoTarget, STALKER_REACTOR_CRITICAL_SOUND )
+
+ AI_CreateDangerousArea_DamageDef( damagedef_stalker_powersupply_explosion_small, nukeFXInfoTarget, TEAM_INVALID, true, false )
+
+ entity fx = PlayFXOnEntity( STALKER_REACTOR_CRITICAL_FX, nukeFXInfoTarget )
+
+ OnThreadEnd(
+ function() : ( nukeFXInfoTarget, fx, npc, attacker )
+ {
+ if ( IsValid( npc ) )
+ StopSoundOnEntity( nukeFXInfoTarget, STALKER_REACTOR_CRITICAL_SOUND )
+
+ if ( IsValid( nukeFXInfoTarget ) )
+ nukeFXInfoTarget.Destroy()
+
+ if ( IsValid( fx ) )
+ fx.Destroy()
+
+ if ( IsAlive( npc ) )
+ {
+ entity damageAttacker
+ if ( IsValid( attacker ) )
+ damageAttacker = attacker
+ else
+ damageAttacker = npc
+
+ vector force = GetDeathForce()
+ npc.Die( damageAttacker, npc, { force = force, scriptType = DF_GIB, damageSourceId = eDamageSourceId.suicideSpectreAoE } )
+ }
+ }
+ )
+
+ wait 1.0
+
+ float duration = 2.1
+ float endTime = Time() + duration
+ float startTime = Time()
+
+ int tagID = npc.LookupAttachment( "CHESTFOCUS" )
+
+ for ( ;; )
+ {
+ float timePassed = Time() - startTime
+ float explodeMin = Graph( timePassed, 0, duration, 0.4, 0.1 )
+ float explodeMax = explodeMin + Graph( timePassed, 0, duration, 0.21, 0.1 )
+ wait RandomFloatRange( explodeMin, explodeMax )
+
+ entity damageAttacker = GetNPCAttackerEnt( npc, attacker )
+
+ // origin = npc.GetWorldSpaceCenter()
+ vector origin = npc.GetAttachmentOrigin( tagID )
+
+ if ( Time() >= endTime )
+ {
+ Explosion_DamageDefSimple( damagedef_stalker_powersupply_explosion_large, origin, damageAttacker, npc, origin )
+ break
+ }
+ else
+ {
+ Explosion_DamageDefSimple( damagedef_stalker_powersupply_explosion_small, origin, damageAttacker, npc, origin )
+ }
+ }
+}
+
+bool function StalkerCanCrawl( entity npc )
+{
+ if ( !IsAlive( npc ) )
+ return false
+
+ if ( npc.Anim_IsActive() )
+ return false
+
+ return true
+}
+
+bool function TryLegBlownOff( entity npc, var damageInfo, int hitGroup, float limbHealthPercentOfMax, string leg, array<string> fxTags, string sound )
+{
+ if ( IsCrawling( npc ) )
+ {
+ // can blow off leg if stalker is already crawling
+ StalkerLimbBlownOff( npc, damageInfo, hitGroup, limbHealthPercentOfMax, leg, fxTags, sound )
+ return true
+ }
+
+ if ( !StalkerCanCrawl( npc ) )
+ return true
+
+ if ( StalkerLimbBlownOff( npc, damageInfo, hitGroup, limbHealthPercentOfMax, leg, fxTags, sound ) )
+ {
+ thread FallAndBecomeCrawlingStalker( npc )
+ return false
+ }
+
+ return true
+}
+
+void function EnableStalkerCrawlingBehavior( entity npc )
+{
+ Assert( StalkerCanCrawl( npc ) )
+ Assert( !IsCrawling( npc ) )
+
+ DisableLeeching( npc )
+
+ DisableMinionUsesHeavyWeapons( npc )
+
+ string crawlingSettings = string ( npc.Dev_GetAISettingByKeyField( "crawlingSettingsWrapper" ) )
+
+ // Changing the setting file includes changing the behavior file to "behavior_stalker_crawling"
+ SetAISettingsWrapper( npc, crawlingSettings )
+
+ npc.ai.crawling = true
+ npc.ai.startCrawlingTime = Time()
+ npc.DisableGrappleAttachment()
+ npc.EnableNPCMoveFlag( NPCMF_DISABLE_ARRIVALS )
+ npc.SetCapabilityFlag( bits_CAP_MOVE_TRAVERSE | bits_CAP_MOVE_SHOOT | bits_CAP_WEAPON_RANGE_ATTACK1 | bits_CAP_AIM_GUN, false )
+ npc.SetActivityModifier( ACT_MODIFIER_CRAWL, true )
+ npc.SetActivityModifier( ACT_MODIFIER_STAGGER, false )
+ npc.SetCanBeGroundExecuted( true )
+ npc.ClearMoveAnim()
+
+ npc.SetHealth( npc.GetMaxHealth() * 0.5 )
+
+ npc.SetAimAssistForcePullPitchEnabled( true )
+
+ thread SelfTerminateAfterDelay( npc )
+}
+
+void function SelfTerminateAfterDelay( entity npc )
+{
+ const float lifeSupportDuration = 8
+ float deathTime = Time() + (lifeSupportDuration * 2)
+
+ npc.EndSignal( "OnDeath" )
+ for ( ;; )
+ {
+ entity enemy = npc.GetEnemy()
+ if ( IsAlive( enemy ) )
+ {
+ if ( Distance( npc.GetEnemyLKP(), npc.GetOrigin() ) < 500 )
+ {
+ if ( npc.TimeSinceSeen( enemy ) < 3 )
+ deathTime = max( Time() + lifeSupportDuration, deathTime )
+ }
+ }
+
+ if ( Time() > deathTime )
+ {
+ npc.Die()
+ return
+ }
+
+ wait 1.0
+ }
+}
+
+void function FallAndBecomeCrawlingStalker( entity npc )
+{
+ // finish what he's doing
+ npc.EndSignal( "OnDeath" )
+
+ npc.ai.transitioningToCrawl = true
+
+ // Workaround for Bug 114372
+ WaitFrame()
+
+ for ( ;; )
+ {
+ if ( npc.IsInterruptable() )
+ break
+ WaitFrame()
+ }
+
+ if ( !StalkerCanCrawl( npc ) )
+ return
+
+ if ( IsCrawling( npc ) )
+ return
+
+ EnableStalkerCrawlingBehavior( npc )
+
+ npc.Anim_Stop() // stop leeching, etc.
+
+ PlayCrawlingAnim( npc, "ACT_STAND_TO_CRAWL" )
+}
+
+void function PlayCrawlingAnim( entity npc, string animation )
+{
+ npc.Anim_ScriptedPlayActivityByName( animation, true, 0.1 )
+ npc.UseSequenceBounds( true )
+}
+
+void function AttemptStandToStaggerAnimation( entity npc )
+{
+ // Check if we are already staggered
+ if ( npc.IsActivityModifierActive( ACT_MODIFIER_STAGGER ) )
+ return
+
+ if ( !npc.IsInterruptable() )
+ return
+
+ if ( npc.ContextAction_IsBusy() )
+ return
+
+ // Are we blocking additional pain animations
+ if ( npc.GetNPCFlag( NPC_NO_PAIN ) )
+ return
+
+ // finish what he's doing
+ npc.EndSignal( "OnDeath" )
+
+ // Workaround for Bug 114372
+ WaitFrame()
+
+ for ( ;; )
+ {
+ if ( npc.IsInterruptable() )
+ break
+
+ WaitFrame()
+ }
+
+ if ( IsCrawling( npc ) || npc.ai.transitioningToCrawl )
+ return
+
+ npc.Anim_ScriptedPlayActivityByName( "ACT_STAND_TO_STAGGER", true, 0.1 )
+ npc.UseSequenceBounds( true )
+ npc.EnableNPCFlag( NPC_PAIN_IN_SCRIPTED_ANIM )
+}
+
+bool function IsStalkerLimbBlownOff( entity npc, string limbName )
+{
+ int bodyGroup = npc.FindBodyGroup( limbName )
+ if ( npc.GetBodyGroupState( bodyGroup ) != 0 )
+ return true
+
+ return false
+}
+
+bool function StalkerLimbBlownOff( entity npc, var damageInfo, int hitGroup, float limbHealthPercentOfMax, string limbName, array<string> fxTags, string sound )
+{
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ switch ( damageSourceId )
+ {
+ case eDamageSourceId.mp_weapon_grenade_emp:
+ case eDamageSourceId.mp_weapon_proximity_mine:
+ return false
+ }
+
+ int bodyGroup = npc.FindBodyGroup( limbName )
+ if ( bodyGroup == -1 )
+ return false
+
+ if ( IsStalkerLimbBlownOff( npc, limbName ) )
+ return false
+
+ EmitSoundOnEntity( npc, sound )
+
+ // blow off limb
+ npc.SetBodygroup( bodyGroup, 1 )
+
+ return true
+}
+
+void function StalkerHeadShot( entity npc, var damageInfo, int hitGroup )
+{
+ // random chance to blow up head
+// if ( DamageInfo_GetDamage( damageInfo ) < 100 && RandomFloat( 100 ) <= 66 )
+// return
+
+ if ( !IsValidHeadShot( damageInfo, npc ) )
+ return
+
+ // only players score headshots
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsAlive( attacker ) )
+ return
+ if ( !attacker.IsPlayer() )
+ return
+
+ if ( DamageInfo_GetDamage( damageInfo ) < npc.GetHealth() )
+ {
+ // force lethal if we have done more than this much damage
+ if ( npc.ai.stalkerHitgroupDamageAccumulated[ hitGroup ] < npc.GetMaxHealth() * STALKER_DAMAGE_REQUIRED_TO_HEADSHOT )
+ return
+ }
+
+ npc.Anim_Stop() // stop leeching, etc.
+ npc.ClearParent()
+
+ //DisableLeeching( npc )
+
+ // No pain anims
+ //DamageInfo_AddDamageFlags( damageInfo, DAMAGEFLAG_NOPAIN )
+
+ // Set these so cl_player knows to kill the eye glow and play the right SFX
+ DamageInfo_AddCustomDamageType( damageInfo, DF_HEADSHOT )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_KILLSHOT )
+
+ EmitSoundOnEntityExceptToPlayer( npc, attacker, "SuicideSpectre.BulletImpact_HeadShot_3P_vs_3P" )
+
+ int bodyGroupIndex = npc.FindBodyGroup( "removableHead" )
+ int stateIndex = 1 // 0 = show, 1 = hide
+ npc.SetBodygroup( bodyGroupIndex, stateIndex )
+
+ DamageInfo_SetDamage( damageInfo, npc.GetMaxHealth() )
+}
+
+vector function GetDeathForce()
+{
+ vector angles = <RandomFloatRange(-45,-75),RandomFloat(360),0>
+ vector forward = AnglesToForward( angles )
+ return forward * RandomFloatRange( 0.25, 0.75 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stationary_firing_positions.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stationary_firing_positions.gnut
new file mode 100644
index 000000000..50b6cc759
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stationary_firing_positions.gnut
@@ -0,0 +1,261 @@
+global function AddStationaryAIPosition //Add stationary positions to pending list.
+global function AddTestTargetPosForStationaryPositionValidation //Add test target location for validating stationary positions.
+global function ValidateAndFinalizePendingStationaryPositions //Runs error-checking/validation logic on stationary positions and finalizes them for use by AI.
+global function GetRandomStationaryPosition
+global function GetClosestAvailableStationaryPosition
+global function ClaimStationaryAIPosition
+global function ReleaseStationaryAIPosition
+
+global enum eStationaryAIPositionTypes
+{
+ MORTAR_TITAN,
+ MORTAR_SPECTRE,
+ SNIPER_TITAN,
+ LAUNCHER_REAPER
+}
+
+global struct StationaryAIPosition
+{
+ vector origin
+ bool inUse
+}
+
+global struct ArrayDistanceEntryForStationaryAIPosition
+{
+ float distanceSqr
+ StationaryAIPosition& ent
+ vector origin
+}
+
+struct
+{
+ array<vector> validationTestTargets
+ table<int, array<vector> > pendingPositions
+ table<int, array<StationaryAIPosition> > stationaryPositions
+} file
+
+void function AddTestTargetPosForStationaryPositionValidation( vector origin )
+{
+ file.validationTestTargets.append( origin )
+}
+
+void function AddStationaryAIPosition( vector origin, int type )
+{
+ AddPendingStationaryAIPosition_Internal( origin, type )
+}
+
+void function AddStationaryAIPosition_Internal( vector origin, int type )
+{
+ StationaryAIPosition pos
+ pos.origin = origin
+ pos.inUse = false
+
+ //Throw warnings for bad positions
+ foreach ( vector testTarget in file.validationTestTargets )
+ {
+ switch( type )
+ {
+ case eStationaryAIPositionTypes.MORTAR_TITAN:
+ if ( NavMesh_ClampPointForHullWithExtents( origin, HULL_TITAN, <100, 100, 20> ) == null )
+ {
+ CodeWarning( "Mortar Titan Firing Position at " + origin + " does not have enough space to accomidate Titan, skipping." )
+ return
+ }
+ break
+
+ #if MP
+ case eStationaryAIPositionTypes.MORTAR_SPECTRE:
+
+ array<vector> testLocations = MortarSpectreGetSquadFiringPositions( origin, testTarget )
+
+ foreach ( vector testLocation in testLocations )
+ {
+ if ( NavMesh_ClampPointForHullWithExtents( testLocation, HULL_HUMAN, <100, 100, 20> ) == null )
+ {
+ CodeWarning( "Mortar Spectre Firing Position at " + origin + " does not have enough space to accomidate squad, skipping." )
+ return
+ }
+ }
+
+ break
+ #endif //MP
+
+ case eStationaryAIPositionTypes.SNIPER_TITAN:
+ if ( NavMesh_ClampPointForHullWithExtents( origin, HULL_TITAN, <100, 100, 20> ) == null )
+ {
+ CodeWarning( "Sniper Titan Firing Position at " + origin + " does not have enough space to accomidate Titan, skipping." )
+ return
+ }
+ break
+
+ case eStationaryAIPositionTypes.LAUNCHER_REAPER:
+ if ( NavMesh_ClampPointForHullWithExtents( origin, HULL_MEDIUM, <100, 100, 20> ) == null )
+ {
+ CodeWarning( "Tick Launching Reaper Firing Position at " + origin + " does not have enough space to accomidate Reaper, skipping." )
+ return
+ }
+ break
+ }
+ }
+
+ if ( !( type in file.stationaryPositions ) )
+ {
+ file.stationaryPositions[ type ] <- []
+ }
+
+ file.stationaryPositions[ type ].append( pos )
+}
+
+//Function tests stationary AI positions for given type relative to given mortar target.
+void function AddPendingStationaryAIPosition_Internal( vector origin, int type )
+{
+ if ( !( type in file.pendingPositions ) )
+ file.pendingPositions[ type ] <- []
+
+ //Add position to table so we can validate and add it when all entities finish loading.
+ file.pendingPositions[ type ].append( origin )
+}
+
+void function ValidateAndFinalizePendingStationaryPositions()
+{
+
+ Assert( file.validationTestTargets.len(), "Test targets are required to validate stationary positions. Use AddTestTargetPosForStationaryPositionValidation to add them before running validation." )
+
+ foreach ( type, origins in file.pendingPositions )
+ {
+ //Make sure we have pending positions for given ai type.
+ Assert( file.pendingPositions[ type ].len(), "Stationary Positions for type " + type + " could not be found in this map. Add Some." )
+
+ foreach ( vector origin in origins )
+ {
+ AddStationaryAIPosition_Internal( origin, type )
+ }
+
+ //Make sure we have positions for given AI type after we validate and finalize positions.
+ Assert( file.stationaryPositions[ type ].len(), "No valid stationary positions for type " + type + " remain after validation. Adjust positions and retry." )
+ }
+}
+
+StationaryAIPosition function GetClosestAvailableStationaryPosition( vector origin, float maxDist, int type )
+{
+
+ array<StationaryAIPosition> resultArray = []
+ float maxDistSqr = maxDist * maxDist
+
+ array<StationaryAIPosition> positions = file.stationaryPositions[type]
+
+ array<ArrayDistanceEntryForStationaryAIPosition> allResults = ArrayDistanceResultsForStationaryAIPosition( positions, origin )
+ allResults.sort( DistanceCompareClosestForStationaryAIPosition )
+
+ //Remove all in use stationary positions up front.
+ array<ArrayDistanceEntryForStationaryAIPosition> freePositions
+ foreach ( result in allResults )
+ {
+ StationaryAIPosition position = result.ent
+ if ( position.inUse )
+ continue
+
+ freePositions.append( result )
+ }
+
+ //Tell us if all spots for a given AI type are taken.
+ Assert( freePositions.len() > 0, "Could not find free mortar positions for type " + type + ", all positions are currently in use. Add more AddStationaryTitanPosition to the map." )
+
+ foreach( result in freePositions )
+ {
+ StationaryAIPosition position = result.ent
+
+ // if too far, throw warning and continue search beyond maxDist
+ if ( result.distanceSqr > maxDistSqr )
+ {
+ CodeWarning( "Couldn't find a mortar position within " + maxDist + " units for type " + type + " around " + origin.tostring() + " that wasn't in use. Expanding Search. Add more AddStationaryTitanPositions to the map near this point." )
+ }
+
+ return position
+ }
+
+ unreachable
+}
+
+StationaryAIPosition function GetRandomStationaryPosition( vector origin, float maxDist, int type )
+{
+ array<StationaryAIPosition> resultArray = []
+ array<StationaryAIPosition> positions = file.stationaryPositions[type]
+
+ //Remove all in use stationary positions up front.
+ array<StationaryAIPosition> freePositions
+ foreach ( position in positions )
+ {
+ if ( position.inUse )
+ continue
+
+ freePositions.append( position )
+ }
+
+ //Tell us if all spots for a given AI type are taken.
+ Assert( freePositions.len() > 0, "Could not find free mortar positions for type " + type + ", all positions are currently in use. Add more AddStationaryTitanPosition to the map." )
+
+ int attemptCount = 1
+ while ( resultArray.len() == 0 )
+ {
+
+ //Expand our search radius each time we reattempt our search.
+ float maxDistSqr = ( maxDist * attemptCount ) * ( maxDist * attemptCount )
+
+ foreach( position in freePositions )
+ {
+ float dist = Distance2DSqr( origin, position.origin )
+ if ( dist <= maxDistSqr )
+ resultArray.append( position )
+ }
+
+ if ( resultArray.len() == 0 )
+ {
+ CodeWarning( "Couldn't find a mortar position within " + maxDist + " units for type " + type + " around " + origin.tostring() + " that wasn't in use. Expanding Search. Add more AddStationaryTitanPositions to the map near this point." )
+ attemptCount += 1
+ }
+ }
+
+ return resultArray.getrandom()
+}
+
+void function ClaimStationaryAIPosition( StationaryAIPosition stationaryTitanPositions )
+{
+ Assert( stationaryTitanPositions.inUse == false )
+ stationaryTitanPositions.inUse = true
+}
+
+void function ReleaseStationaryAIPosition( StationaryAIPosition stationaryTitanPositions )
+{
+ Assert( stationaryTitanPositions.inUse == true )
+ stationaryTitanPositions.inUse = false
+}
+
+array<ArrayDistanceEntryForStationaryAIPosition> function ArrayDistanceResultsForStationaryAIPosition( array<StationaryAIPosition> entArray, vector origin )
+{
+ array<ArrayDistanceEntryForStationaryAIPosition> allResults
+
+ foreach ( ent in entArray )
+ {
+ ArrayDistanceEntryForStationaryAIPosition entry
+
+ vector entOrigin = ent.origin
+ entry.distanceSqr = DistanceSqr( entOrigin, origin )
+ entry.ent = ent
+ entry.origin = entOrigin
+
+ allResults.append( entry )
+ }
+
+ return allResults
+}
+
+int function DistanceCompareClosestForStationaryAIPosition( ArrayDistanceEntryForStationaryAIPosition a, ArrayDistanceEntryForStationaryAIPosition b )
+{
+ if ( a.distanceSqr > b.distanceSqr )
+ return 1
+ else if ( a.distanceSqr < b.distanceSqr )
+ return -1
+
+ return 0;
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_suicide_spectres.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_suicide_spectres.gnut
new file mode 100644
index 000000000..f8e0652ce
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_suicide_spectres.gnut
@@ -0,0 +1,576 @@
+global function SuicideSpectres_Init
+global function MakeSuicideSpectre
+global function SpectreSuicideOnDamaged
+global function GetNPCAttackerEnt
+
+const FX_SPECTRE_EXPLOSION = $"P_drone_frag_exp"
+
+//
+// Suicide spectre script
+//
+
+const SPECTRE_EXPLOSION_DELAY = 0.25 // Delay for the first spectre in a chain to start exploding.
+const SPECTRE_DAMAGE_MULTIPLIER_BODY = 1.5
+const SPECTRE_DAMAGE_MULTIPLIER_HEAD = 6.0
+const SPECTRE_DAMAGE_MULTIPLIER_SMART_PISTOL = 2.0
+const SPECTRE_HEADSHOT_KEEP_WALKING_CHANCE = 100 // 35% chance to keep walking after a headshot to add variety
+
+struct
+{
+ int chainExplosionIndex
+ float lastChainExplosionTime
+
+ table< string, array<string> > spectreAnims
+ float nextOverloadTime
+
+} file
+
+const SFX_TICK_OVERLOAD = "corporate_spectre_overload_beep"
+const SFX_TICK_EXPLODE = "corporate_spectre_death_explode"
+
+const SFX_FRAGDRONE_OVERLOAD = "weapon_sentryfragdrone_preexplo"
+const SFX_FRAGDRONE_EXPLODE = "weapon_sentryfragdrone_explo"
+const SFX_FRAGDRONE_SUPERPURSUIT = "weapon_sentryfragdrone_superpursuit"
+
+const CHAIN_EXPLOSION_MAXINDEX = 10
+
+
+void function SuicideSpectres_Init()
+{
+ RegisterSignal( "SuicideSpectreForceExplode" )
+ RegisterSignal( "SuicideSpectreExploding" )
+ RegisterSignal( "SuicideGotEnemy" )
+ RegisterSignal( "SuicideLostEnemy" )
+
+ PrecacheParticleSystem( FX_SPECTRE_EXPLOSION )
+
+ file.spectreAnims[ "spectreSearch" ] <- []
+ file.spectreAnims[ "spectreSearch" ].append( "sp_suicide_spectre_search" )
+ file.spectreAnims[ "spectreSearch" ].append( "sp_suicide_spectre_search_B" )
+ file.spectreAnims[ "spectreSearch" ].append( "sp_suicide_spectre_search_C" )
+
+ AddDamageCallback( "npc_frag_drone", SpectreSuicideOnDamaged_Callback )
+ AddDeathCallback( "npc_frag_drone", FragDroneDeath )
+}
+
+/************************************************************************************************\
+
+ ###### ######## ######## ## ## ########
+## ## ## ## ## ## ## ##
+## ## ## ## ## ## ##
+ ###### ###### ## ## ## ########
+ ## ## ## ## ## ##
+## ## ## ## ## ## ##
+ ###### ######## ## ####### ##
+
+\************************************************************************************************/
+void function MakeSuicideSpectre( entity spectre )
+{
+ spectre.SetAimAssistAllowed( true )
+ spectre.SetAllowMelee( false )
+ DisableLeeching( spectre )
+
+ spectre.SetNPCMoveSpeedScale( 1.0 )
+
+ spectre.EnableNPCMoveFlag( NPCMF_IGNORE_CLUSTER_DANGER_TIME | NPCMF_PREFER_SPRINT )
+ spectre.DisableNPCMoveFlag( NPCMF_FOLLOW_SAFE_PATHS | NPCMF_INDOOR_ACTIVITY_OVERRIDE )
+
+ spectre.kv.allowShoot = 0
+
+ // Frag drones do suicide spectre behavior but we don't want them doing the enemy changed sounds so filter them out
+ if ( !IsFragDrone( spectre ) && !IsTick( spectre ) )
+ spectre.SetEnemyChangeCallback( SuicideSpectreEnemyChanged )
+
+ spectre.SetLookDistOverride( SPECTRE_MAX_SIGHT_DIST )
+ //spectre.SetHearingSensitivity( 10 ) //1 is default
+ spectre.EnableNPCFlag( NPC_MUTE_TEAMMATE )
+
+ spectre.ai.suicideSpectreExplosionDelay = -1
+
+ thread SpectreWaitToExplode( spectre )
+ AddAnimEvent( spectre, "frag_drone_armed", FragDroneArmed )
+}
+
+void function FragDroneArmed( entity npc )
+{
+ npc.ai.fragDroneArmed = true
+}
+
+void function FragDroneDeath( entity spectre, var damageInfo )
+{
+ FragDroneDeath_Think( spectre, damageInfo )
+}
+
+// for reloadscripts
+void function FragDroneDeath_Think( entity spectre, var damageInfo )
+{
+ vector pos = spectre.GetOrigin()
+ int tagID = spectre.LookupAttachment( "CHESTFOCUS" )
+ vector fxOrg = spectre.GetAttachmentOrigin( tagID )
+ string expSFX
+ if ( spectre.mySpawnOptions_aiSettings == "npc_frag_drone_throwable" )
+ expSFX = SFX_FRAGDRONE_EXPLODE
+ else
+ expSFX = SFX_TICK_EXPLODE
+ int expFX = GetParticleSystemIndex( FX_SPECTRE_EXPLOSION )
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity attackerEnt = GetNPCAttackerEnt( spectre, attacker )
+
+ int team = GetExplosionTeamBasedOnGamemode( spectre )
+
+ int damageDef = GetDamageDefForFragDrone( spectre )
+
+ RadiusDamage_DamageDefSimple( damageDef, pos, attackerEnt, spectre, 0 )
+ EmitSoundAtPosition( spectre.GetTeam(), pos, expSFX )
+ CreateShake( pos, 10, 105, 1.25, 768 )
+ StartParticleEffectInWorld( expFX, fxOrg, Vector( 0, 0, 0 ) )
+
+ spectre.Gib( <0, 0, 100> ) //Used to do .Destroy() on the frag drones immediately, but this meant you can't display the obiturary correctly. Instead, since it's dead already just hide it
+}
+
+entity function GetNPCAttackerEnt( entity npc, entity attacker )
+{
+ entity owner = npc.GetBossPlayer()
+ bool ownerIsPlayer = owner != null && owner.IsPlayer()
+
+ if ( IsMultiplayer() )
+ return ownerIsPlayer ? owner : npc
+
+ if ( !IsAlive( attacker ) )
+ return npc
+
+ // dont give player credit, since that does some bad things
+ if ( ownerIsPlayer )
+ return owner
+
+ if ( attacker.IsPlayer() )
+ return GetEnt( "worldspawn" )
+
+ return attacker
+}
+
+
+int function GetDamageDefForFragDrone( entity drone )
+{
+ var damageDef = drone.Dev_GetAISettingByKeyField( "damageDefOverride" )
+ if ( damageDef != null )
+ {
+ expect string( damageDef )
+ return eDamageSourceId[ damageDef ]
+ }
+
+ entity owner = drone.GetBossPlayer()
+ if ( owner != null && owner.IsPlayer() )
+ return damagedef_frag_drone_throwable_PLAYER
+
+ return damagedef_frag_drone_throwable_NPC
+}
+
+void function SuicideSpectreEnemyChanged( entity spectre )
+{
+ // Spectre "Speaks"
+ if ( ( RandomFloat( 1.0 ) ) < 0.02 )
+ EmitSoundOnEntity( spectre, "diag_imc_spectre_gs_spotenemypilot_01_1" )
+}
+
+/************************************************************************************************\
+
+######## ######## ####### ## ## #### ## ## #### ######## ## ##
+## ## ## ## ## ## ## ## ## ### ### ## ## ## ##
+## ## ## ## ## ## ## ## ## #### #### ## ## ####
+######## ######## ## ## ### ## ## ### ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ####### ## ## #### ## ## #### ## ##
+
+\************************************************************************************************/
+void function SpectreWaitToExplode( entity spectre )
+{
+ Assert( spectre.IsNPC() )
+ spectre.EndSignal( "OnDeath" )
+
+ waitthread SuicideSpectre_WaittillNearEnemyOrExploding( spectre )
+
+ if ( spectre.ai.suicideSpectreExplodingAttacker == null )
+ {
+ // not exploding, so overload
+ spectre.ai.suicideSpectreExplosionDelay = GetSpectreExplosionTime( spectre )
+ waitthread SpectreOverloads( spectre )
+ }
+
+ if ( spectre.ai.suicideSpectreExplosionDelay > 0 )
+ wait spectre.ai.suicideSpectreExplosionDelay
+
+ entity attacker = spectre.ai.suicideSpectreExplodingAttacker
+ if ( !IsValid( attacker ) )
+ {
+ entity lastAttacker = GetLastAttacker( spectre )
+ if ( IsValid( lastAttacker ) )
+ {
+ attacker = lastAttacker
+ }
+ else
+ {
+ attacker = spectre
+ }
+ }
+
+ vector force = GetDeathForce()
+
+ Assert( !attacker.IsProjectile(), "Suicide Spectre attacker was a projectile! Type: " + attacker.ProjectileGetWeaponClassName() )
+
+ // JFS: sometimes the attacker is a projectile, which can cause a script error.
+ // The real solution is to figure out which weapon is passing in the projectile as the attacker and correct that.
+ if ( attacker.IsProjectile() )
+ {
+ attacker = spectre
+ }
+
+ spectre.Die( attacker, attacker, { force = force, scriptType = DF_DOOMED_HEALTH_LOSS, damageSourceId = eDamageSourceId.suicideSpectreAoE } )
+}
+
+void function SetSuicideSpectreExploding( entity spectre, entity attacker, float explodingTime )
+{
+ Assert( spectre.ai.suicideSpectreExplodingAttacker == null )
+ spectre.ai.suicideSpectreExplodingAttacker = attacker
+ spectre.ai.suicideSpectreExplosionDelay = explodingTime
+
+ spectre.Signal( "SuicideSpectreExploding" )
+}
+
+float function GetSpectreExplosionTime( entity spectre )
+{
+ if ( Time() - file.lastChainExplosionTime > 1.0 )
+ file.chainExplosionIndex = 0
+
+ float waitTime = file.chainExplosionIndex * 0.14 // RandomFloatRange( CHAIN_EXPLOSION_INTERVALMIN, CHAIN_EXPLOSION_INTERVALMAX )
+ file.lastChainExplosionTime = Time()
+ file.chainExplosionIndex++
+ return waitTime
+}
+
+void function SuicideSpectre_WaittillNearEnemyOrExploding( entity spectre )
+{
+ spectre.EndSignal( "OnDeath" )
+ spectre.EndSignal( "SuicideSpectreExploding" )
+ spectre.EndSignal( "SuicideSpectreForceExplode" )
+
+ bool pursuitSoundPlaying = false
+
+ float minScale = expect float( spectre.Dev_GetAISettingByKeyField( "minSpeedScale" ) )
+ float maxScale = expect float( spectre.Dev_GetAISettingByKeyField( "maxSpeedScale" ) )
+
+ while ( true )
+ {
+ wait 0.1
+
+ if ( !spectre.ai.fragDroneArmed )
+ continue
+
+ if ( spectre.ai.suicideSpectreExplodingAttacker != null )
+ return
+
+ //If spectre is not interrruptable, don't bother
+ if ( !spectre.IsInterruptable() )
+ continue
+
+ //If spectre is parented, don't bother
+ if ( IsValid( spectre.GetParent() ) )
+ continue
+
+ // speed up when near enemy
+ entity enemy = spectre.GetEnemy()
+ if ( IsAlive( enemy ) )
+ {
+ float dist = Distance( enemy.GetOrigin(), spectre.GetOrigin() )
+ float maxDist = 850
+ if ( spectre.mySpawnOptions_aiSettings == "npc_frag_drone_throwable" )
+ {
+ if ( dist < maxDist )
+ {
+ if ( pursuitSoundPlaying == false )
+ {
+ EmitSoundOnEntity( spectre, SFX_FRAGDRONE_SUPERPURSUIT )
+ pursuitSoundPlaying = true
+ }
+ }
+ else
+ {
+ if ( pursuitSoundPlaying == true )
+ {
+ StopSoundOnEntity( spectre, SFX_FRAGDRONE_SUPERPURSUIT )
+ pursuitSoundPlaying = false
+ }
+ }
+ }
+ float speed = GraphCapped( dist, 200, 850, maxScale, minScale )
+ spectre.SetNPCMoveSpeedScale( speed )
+ }
+
+ // offset the overload time
+ if ( Time() < file.nextOverloadTime )
+ continue
+
+ entity attacker = SuicideSpectre_NearEnemy( spectre )
+ if ( attacker != null )
+ {
+ //SetSuicideSpectreOverloading( spectre, attacker )
+ //Assert( 0 ) // never reached
+ return
+ }
+ }
+}
+
+entity function SuicideSpectre_NearEnemy( entity spectre )
+{
+ // See if any player is close eneough to trigger self-destruct
+ array<entity> enemies
+ entity closestEnemy = spectre.GetClosestEnemy()
+ if ( closestEnemy )
+ enemies.append( closestEnemy )
+
+ entity currentEnemy = spectre.GetEnemy()
+ if ( currentEnemy && currentEnemy != closestEnemy )
+ enemies.append( currentEnemy )
+
+ vector origin = spectre.GetOrigin()
+ float dist = expect float( spectre.Dev_GetAISettingByKeyField( "suicideExplosionDistance" ) )
+ foreach ( enemy in enemies )
+ {
+ if ( !IsAlive( enemy ) )
+ continue
+ if ( enemy.IsCloaked( true ) )
+ continue
+ if ( enemy.GetNoTarget() )
+ continue
+ if ( enemy.IsPlayer() && enemy.IsPhaseShifted() )
+ continue
+
+ vector enemyOrigin = enemy.GetOrigin()
+
+ if ( Distance( origin, enemyOrigin ) > dist )
+ continue
+
+ float heightDiff = enemyOrigin.z - origin.z
+
+ // dont explode because you jump over me or I am on the floor above you
+ if ( fabs( heightDiff ) > 40 )
+ {
+ // unless enemy is standing on something slightly above you and there is a clear trace
+ float curTime = Time()
+ float timeDiff = curTime - spectre.ai.suicideSpectreExplosionTraceTime
+ const float TRACE_INTERVAL = 2
+
+ if ( heightDiff > 0 && timeDiff > TRACE_INTERVAL && enemy.IsOnGround() && spectre.CanSee( enemy ) )
+ {
+ spectre.ai.suicideSpectreExplosionTraceTime = curTime
+ float frac = TraceHullSimple( origin, < origin.x, origin.y, enemyOrigin.z >, spectre.GetBoundingMins(), spectre.GetBoundingMaxs(), spectre )
+ if ( frac == 1.0 )
+ return enemy
+ }
+ continue
+ }
+
+ return enemy
+ }
+
+ return null
+}
+
+void function SpectreOverloads( entity spectre )
+{
+ spectre.EndSignal( "SuicideSpectreExploding" )
+ file.nextOverloadTime = Time() + 0.05
+
+ #if MP
+ var chaseTime = spectre.Dev_GetAISettingByKeyField( "SuicideChaseTime" )
+ if ( chaseTime != null )
+ {
+ float maxScale = expect float( spectre.Dev_GetAISettingByKeyField( "maxSpeedScale" ) )
+ spectre.SetNPCMoveSpeedScale( maxScale )
+
+ expect float( chaseTime )
+ float endChaseTime = Time() + chaseTime
+
+ for ( ;; )
+ {
+ if ( Time() >= endChaseTime )
+ break
+
+ if ( !IsAlive( spectre.GetEnemy() ) )
+ break
+
+ entity nearEnemy = SuicideSpectre_NearEnemy( spectre )
+ if ( IsAlive( nearEnemy ) )
+ {
+ if ( nearEnemy.IsTitan() && spectre.IsInterruptable() )
+ {
+ JumpAtTitan( spectre, nearEnemy )
+ spectre.ai.suicideSpectreExplosionDelay = 0.0
+ return
+ }
+ break
+ }
+
+ WaitFrame()
+ }
+ }
+ #endif
+
+ for ( ;; )
+ {
+ #if SP
+ if ( spectre.IsInterruptable() && !spectre.Anim_IsActive() )
+ break
+ #elseif MP
+ if ( spectre.IsInterruptable() && !spectre.Anim_IsActive() && spectre.IsOnGround() )
+ break
+ #endif
+
+ WaitFrame()
+ }
+
+ string overloadSF
+ bool isFragDrone = spectre.mySpawnOptions_aiSettings == "npc_frag_drone_throwable"
+ if ( isFragDrone )
+ overloadSF = SFX_FRAGDRONE_OVERLOAD
+ else
+ overloadSF = SFX_TICK_OVERLOAD
+ // Overload Sound
+ EmitSoundOnEntity( spectre, overloadSF )
+
+ AI_CreateDangerousArea_DamageDef( damagedef_frag_drone_explode, spectre, TEAM_INVALID, true, false )
+
+ // Cleanup on thread end
+ OnThreadEnd(
+ function() : ( spectre, overloadSF )
+ {
+ if ( IsValid( spectre ) )
+ {
+ StopSoundOnEntity( spectre, overloadSF )
+ }
+ }
+ )
+
+ bool jumpAtTitans = spectre.Dev_GetAISettingByKeyField( "JumpAtTitans" ) == null || spectre.Dev_GetAISettingByKeyField( "JumpAtTitans" ) == 1
+
+ entity enemy = spectre.GetEnemy()
+ if ( enemy && enemy.IsTitan() && jumpAtTitans && !spectre.IsInterruptable() )
+ {
+ JumpAtTitan( spectre, enemy )
+ }
+ else
+ {
+ string anim = "sp_suicide_spectre_explode_stand"
+ var overrideAnim = spectre.Dev_GetAISettingByKeyField( "OverrideOverloadAnim" )
+
+ if ( overrideAnim != null )
+ {
+ anim = expect string( overrideAnim )
+ }
+
+ waitthread PlayAnim( spectre, anim )
+
+ if ( !isFragDrone )
+ wait 0.25
+ }
+}
+
+void function JumpAtTitan( entity spectre, entity enemy )
+{
+ vector myOrigin = spectre.GetOrigin()
+ vector dirToEnemy = enemy.EyePosition() - myOrigin
+
+ float dist = Length( dirToEnemy )
+ if ( dist > 0 )
+ {
+ const float MAX_DIST = 100
+ dirToEnemy *= min( MAX_DIST, dist ) / dist
+ }
+
+ vector refOrigin = myOrigin + Vector( dirToEnemy.x, dirToEnemy.y, 256 )
+ vector refAngles = spectre.GetAngles() + Vector( 0, 180, 0 )
+ spectre.Anim_ScriptedPlayWithRefPoint( "sd_jump_explode", refOrigin, refAngles, 0.3 )
+ WaittillAnimDone( spectre )
+ return
+}
+
+int function GetExplosionTeamBasedOnGamemode( entity spectre )
+{
+ return spectre.GetTeam()
+}
+
+
+/************************************************************************************************\
+
+######## ### ## ## ### ###### ########
+## ## ## ## ### ### ## ## ## ## ##
+## ## ## ## #### #### ## ## ## ##
+## ## ## ## ## ### ## ## ## ## #### ######
+## ## ######### ## ## ######### ## ## ##
+## ## ## ## ## ## ## ## ## ## ##
+######## ## ## ## ## ## ## ###### ########
+
+\************************************************************************************************/
+void function SpectreSuicideOnDamaged_Callback( entity spectre, var damageInfo )
+{
+ SpectreSuicideOnDamaged( spectre, damageInfo )
+}
+
+
+void function SpectreSuicideOnDamaged( entity spectre, var damageInfo )
+{
+ //Assert( IsSuicideSpectre( spectre ) )
+
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ DamageInfo_SetCustomDamageType( damageInfo, damageType )
+
+ if ( !IsAlive( spectre ) )
+ return
+
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int damageSourceId = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ // Calculate build time credit
+ if ( attacker.IsPlayer() )
+ {
+ if ( GameModeRulesShouldGiveTimerCredit( attacker, spectre, damageInfo ) && !TitanDamageRewardsTitanCoreTime() )
+ {
+ float timerCredit = CalculateBuildTimeCredit( attacker, spectre, damage, spectre.GetHealth(), spectre.GetMaxHealth(), "spectre_kill_credit", 9 )
+ if ( timerCredit )
+ DecrementBuildTimer( attacker, timerCredit )
+ }
+ }
+
+ // No pain anims for suicide spectres
+ DamageInfo_AddDamageFlags( damageInfo, DAMAGEFLAG_NOPAIN )
+
+
+ spectre.Signal( "SuicideSpectreExploding" )
+
+ if ( !IsValid( inflictor ) || !inflictor.IsPlayer() )
+ {
+ if ( spectre.ai.suicideSpectreExplodingAttacker == null )
+ {
+ if ( spectre.GetHealth() - damage <= 0 || ( IsValid( inflictor ) && IsTick( inflictor ) ) )
+ {
+ float explosionTime = GetSpectreExplosionTime( spectre )
+ SetSuicideSpectreExploding( spectre, attacker, explosionTime )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ }
+ else
+ {
+ // already exploding
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ DamageInfo_SetDamage( damageInfo, damage )
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret.gnut
new file mode 100644
index 000000000..eca5849bf
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret.gnut
@@ -0,0 +1,24 @@
+global function AiTurret_Init
+global function GetMegaTurretLinkedToPanel
+global function MegaTurretUsabilityFunc
+global function SetUsePromptForPanel
+
+void function AiTurret_Init()
+{
+
+}
+
+entity function GetMegaTurretLinkedToPanel(entity panel)
+{
+ return null
+}
+
+string function MegaTurretUsabilityFunc(var turret, var panel)
+{
+ return "pilot"
+}
+
+void function SetUsePromptForPanel(var panel, var turret)
+{
+
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret_sentry.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret_sentry.gnut
new file mode 100644
index 000000000..e34b30826
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_turret_sentry.gnut
@@ -0,0 +1,72 @@
+global function AiTurretSentry_Init
+
+const DEAD_SENTRY_TURRET_FX = $"P_impact_exp_med_air"
+const DEAD_SENTRY_TURRET_SFX = "SentryTurret_DeathExplo"
+const SENTRY_TURRET_AIM_FX_RED = $"P_wpn_lasercannon_aim_short"
+const SENTRY_TURRET_AIM_FX_BLUE = $"P_wpn_lasercannon_aim_short_blue"
+
+void function AiTurretSentry_Init()
+{
+ PrecacheParticleSystem( DEAD_SENTRY_TURRET_FX )
+ //PrecacheParticleSystem( SENTRY_TURRET_AIM_FX_RED )
+ //PrecacheParticleSystem( SENTRY_TURRET_AIM_FX_BLUE )
+ //PrecacheParticleSystem( SENTRY_TURRET_AIM_FX2 )
+
+ AddSpawnCallback( "npc_turret_sentry", LightTurretSpawnFunction )
+ AddDeathCallback( "npc_turret_sentry", LightTurretDeathFX )
+
+ //RegisterSignal( "TurretDisabled" )
+ //RegisterSignal( "HandleTargetDeath" )
+ //RegisterSignal( "OnPlayerDisconnectResetTurret" )
+ //RegisterSignal( "Deactivate_Turret" )
+ //RegisterSignal( "TurretShieldWallRelease")
+ //RegisterSignal( "DestroyShieldFX")
+}
+
+void function LightTurretDeathFX( entity turret, var damageInfo )
+{
+ turret.SetBodygroup( 0, 1 )
+
+ int turretEHandle = turret.GetEncodedEHandle()
+ array<entity> players = GetPlayerArray()
+ foreach( player in players )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_TurretRefresh", turretEHandle )
+ }
+
+ EmitSoundAtPosition( turret.GetTeam(), turret.GetOrigin(), DEAD_SENTRY_TURRET_SFX )
+ PlayFX( DEAD_SENTRY_TURRET_FX, turret.GetOrigin() + Vector( 0,0,38 ) ) // played with a slight offset as requested by BigRig
+}
+
+//////////////////////////////////////////////////////////
+void function LightTurretSpawnFunction( entity turret )
+{
+ turret.UnsetUsable()
+
+// float windupTime = TurretGetWindupTime( turret )
+// if ( windupTime > 0 )
+// thread HACK_TurretManagePreAttack( turret, OnWindupBegin_SentryTurret, OnWindupEnd_Turret )
+//
+ if ( turret.Dev_GetAISettingByKeyField( "aim_laser_disabled" ) )
+ return
+
+ thread SentryTurretAimLaser( turret )
+}
+
+void function SentryTurretAimLaser( entity turret )
+{
+ entity fx1 = PlayLoopFXOnEntity( SENTRY_TURRET_AIM_FX_RED, turret, "camera_glow", null, null, ENTITY_VISIBLE_TO_ENEMY )
+ entity fx2 = PlayLoopFXOnEntity( SENTRY_TURRET_AIM_FX_BLUE, turret, "camera_glow", null, null, ENTITY_VISIBLE_TO_FRIENDLY )
+
+ OnThreadEnd(
+ function() : ( fx1, fx2 )
+ {
+ if ( IsValid( fx1 ) )
+ EffectStop( fx1 )
+ if ( IsValid( fx2 ) )
+ EffectStop( fx2 )
+ }
+ )
+
+ WaitSignal( turret, "OnDeath" )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_utility.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_utility.gnut
new file mode 100644
index 000000000..67c686003
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_utility.gnut
@@ -0,0 +1,558 @@
+untyped
+
+globalize_all_functions
+
+function AiUtility_Init()
+{
+ RegisterSignal( "OnNewOwner" )
+ RegisterSignal( "squadInCombat" )
+ RegisterSignal( "OnEndFollow" )
+ RegisterSignal( "OnStunned" )
+
+}
+////////////////////////////////////////////////////////////////////////////////
+// Cloaks npc forever (to be used by anim events)
+function NpcCloakOn( npc )
+{
+ //SetCloakDuration( fade in, duration, fade out )
+ npc.SetCloakDuration( 2.0, -1, 0 )
+ EmitSoundOnEntity( npc, CLOAKED_DRONE_CLOAK_START_SFX )
+ EmitSoundOnEntity( npc, CLOAKED_DRONE_CLOAK_LOOP_SFX )
+ npc.Minimap_Hide( TEAM_IMC, null )
+ npc.Minimap_Hide( TEAM_MILITIA, null )
+}
+////////////////////////////////////////////////////////////////////////////////
+// De-cloaks npc
+function NpcCloakOff( npc)
+{
+ npc.SetCloakDuration( 0, 0, 1.5 )
+ StopSoundOnEntity( npc, CLOAKED_DRONE_CLOAK_LOOP_SFX )
+ npc.Minimap_AlwaysShow( TEAM_IMC, null )
+ npc.Minimap_AlwaysShow( TEAM_MILITIA, null )
+}
+
+int function GetDefaultNPCFollowBehavior( npc )
+{
+ switch ( npc.GetAIClass() )
+ {
+ case AIC_FLYING_DRONE:
+ return AIF_SUPPORT_DRONE
+
+ case AIC_VEHICLE:
+ return AIF_GUNSHIP
+
+ case AIC_TITAN:
+ case AIC_TITAN_BUDDY:
+ return AIF_TITAN_FOLLOW_PILOT
+ }
+
+ return AIF_FIRETEAM
+}
+
+void function DieOnPlayerDisconnect( entity npc, entity player )
+{
+ Assert( IsNewThread(), "Must be threaded off" )
+ Assert( npc.IsNPC() )
+ Assert( player.IsPlayer() )
+ Assert( IsAlive( npc ) )
+ Assert( npc.GetBossPlayer() == player )
+ Assert( !IsDisconnected( player ) )
+ npc.EndSignal( "OnDeath" )
+
+ player.WaitSignal( "OnDestroy" )
+
+ // my boss quit the server!
+ if ( IsAlive( npc ) && npc.GetBossPlayer() == player )
+ npc.Die()
+}
+
+void function NPCFollowsPlayer( entity npc, entity leader )
+{
+ Assert( IsAlive( npc ) )
+ Assert( leader.IsPlayer() )
+
+ npc.SetBossPlayer( leader )
+
+ // team
+ SetTeam( npc, leader.GetTeam() )
+
+ if ( IsSpectre( npc ) )
+ {
+ string squadName = GetPlayerSpectreSquadName( leader )
+ SetSquad( npc, squadName )
+ }
+
+ thread DieOnPlayerDisconnect( npc, leader )
+ #if SP
+ Highlight_SetFriendlyHighlight( npc, "friendly_ai" )
+ #else
+ Highlight_SetOwnedHighlight( npc, "friendly_ai" )
+ #endif
+
+ NpcFollowsEntity( npc, leader )
+}
+
+void function NPCFollowsNPC( entity npc, entity leader )
+{
+ Assert( IsAlive( npc ) )
+ Assert( IsAlive( leader ) )
+ Assert( leader.IsNPC() )
+
+ // team
+ SetTeam( npc, leader.GetTeam() )
+
+ // squad
+ string squadNameOwner = expect string( leader.Get( "squadname" ) )
+ if ( squadNameOwner != "" && leader.GetClassName() == npc.GetClassName() )
+ SetSquad( npc, squadNameOwner )
+
+ NpcFollowsEntity( npc, leader )
+}
+
+void function NpcFollowsEntity( entity npc, entity leader )
+{
+ // stop scripted things
+ if ( IsMultiplayer() )
+ npc.Signal( "StopHardpointBehavior" )
+
+ if ( leader.IsPlayer() && leader.p.followPlayerOverride != null )
+ {
+ leader.p.followPlayerOverride( npc, leader )
+ return
+ }
+
+ // follow!
+ int followBehavior = GetDefaultNPCFollowBehavior( npc )
+ npc.InitFollowBehavior( leader, followBehavior )
+ npc.DisableBehavior( "Assault" )
+ npc.DisableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_USE_SHOOTING_COVER )
+ npc.EnableBehavior( "Follow" )
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////
+bool function HasEnemyWithinDist( entity npc, float dist )
+{
+ float distSq = dist * dist
+
+ array<entity> enemies
+ entity closestEnemy = npc.GetClosestEnemy()
+ if ( closestEnemy )
+ enemies.append( closestEnemy )
+
+ entity currentEnemy = npc.GetEnemy()
+ if ( currentEnemy && currentEnemy != closestEnemy )
+ enemies.append( currentEnemy )
+
+ if ( !enemies.len() )
+ return false
+
+ vector origin = npc.GetOrigin()
+ foreach ( enemy in enemies )
+ {
+ if ( DistanceSqr( origin, enemy.GetOrigin() ) < distSq )
+ return true
+ }
+
+ return false
+}
+
+SpawnPointFP function FindSpawnPointForNpcCallin( entity npc, asset model, string anim )
+{
+ float yaw = npc.EyeAngles().y
+
+ vector npcView = AnglesToForward( npc.EyeAngles() )
+ FlightPath flightPath = GetAnalysisForModel( model, anim )
+
+ CallinData drop
+ InitCallinData( drop )
+ SetCallinStyle( drop, eDropStyle.NEAREST_YAW_FALLBACK )
+ SetCallinOwnerEyePos( drop, npc.EyePosition() )
+ drop.dist = 800
+ drop.origin = npc.GetOrigin() + npcView * 250
+ drop.yaw = yaw
+
+ vector angles = Vector( 0, yaw, 0 )
+ SpawnPointFP spawnPoint = GetSpawnPointForStyle( flightPath, drop )
+ if ( spawnPoint.valid )
+ return spawnPoint
+
+ //if it didn't find one where he was looking - try near him
+ drop.origin = npc.GetOrigin()
+ spawnPoint = GetSpawnPointForStyle( flightPath, drop )
+
+ return spawnPoint
+}
+
+function WaitForSquadInCombat( squad )
+{
+ local master = {}
+
+ //when the thread ends, let child threads now
+ OnThreadEnd(
+ function() : ( master )
+ {
+ Signal( master, "OnDestroy" )
+ }
+ )
+
+ // this internal function keeps track of each guy
+ local combatTracker =
+ function( guy, master )
+ {
+ expect entity( guy )
+ expect entity( master )
+
+ EndSignal( master, "OnDestroy" )
+ EndSignal( guy, "OnDeath", "OnDestroy" )
+ if ( !IsAlive( guy ) )
+ return
+
+ while ( guy.GetNPCState() != "combat" )
+ guy.WaitSignal( "OnStateChange" )
+
+ Signal( master, "squadInCombat" )
+ }
+
+ foreach ( guy in squad )
+ {
+ thread combatTracker( guy, master )
+ }
+
+ WaitSignal( master, "squadInCombat" )
+}
+
+function WaitForNpcInCombat( npc )
+{
+ while ( npc.GetNPCState() != "combat" )
+ npc.WaitSignal( "OnStateChange" )
+}
+
+int function GetNpcHullType( entity npc )
+{
+ string aiSettings = npc.GetAISettingsName()
+ return int ( Dev_GetAISettingByKeyField_Global( aiSettings, "HullType" ) )
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+// "SPAWN AI" DEV MENU Fuctions
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+const float CROSSHAIR_VERT_OFFSET = 32
+
+vector function GetPlayerCrosshairOriginRaw( entity player )
+{
+ vector angles = player.EyeAngles()
+ vector forward = AnglesToForward( angles )
+ vector origin = player.EyePosition()
+
+ vector start = origin
+ vector end = origin + forward * 50000
+ TraceResults result = TraceLine( start, end )
+ vector crosshairOrigin = result.endPos
+
+ return crosshairOrigin
+}
+
+vector function GetPlayerCrosshairOrigin( entity player )
+{
+ return (GetPlayerCrosshairOriginRaw( player ) + Vector( 0, 0, CROSSHAIR_VERT_OFFSET ))
+}
+
+void function DEV_SpawnBTAtCrosshair( bool hotdrop = false )
+{
+ DisablePrecacheErrors()
+ wait 0.2
+ entity player = GetPlayerArray()[ 0 ]
+
+ entity pet_titan = player.GetPetTitan()
+ if ( IsValid(pet_titan) )
+ pet_titan.Destroy()
+
+ vector origin = GetPlayerCrosshairOrigin( player )
+ vector angles = Vector( 0, 0, 0 )
+
+ TitanLoadoutDef loadout = GetTitanLoadoutForCurrentMap()
+ entity npc = CreateAutoTitanForPlayer_FromTitanLoadout( player, loadout, origin, angles )
+
+ SetSpawnOption_AISettings( npc, "npc_titan_buddy" )
+
+ DispatchSpawn( npc )
+
+ SetPlayerPetTitan( player, npc )
+
+ if ( hotdrop )
+ thread NPCTitanHotdrops( npc, false )
+}
+
+void function DEV_SpawnAllNPCsWithTeam( int team )
+{
+ printt( "script thread DEV_SpawnAllNPCsWithTeam( " + team + " )" )
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+ bool restoreHostThreadMode = GetConVarInt( "host_thread_mode" ) != 0
+ if ( restoreHostThreadMode )
+ {
+ DisablePrecacheErrors()
+ wait 0.5
+ }
+
+ entity player = GetPlayerArray()[ 0 ]
+ vector origin = GetPlayerCrosshairOrigin( player )
+ array<string> aiSettings = GetAllNPCSettings()
+
+ foreach ( settings in aiSettings )
+ {
+ vector angles = < 0, RandomFloat( 360 ), 0 >
+ entity npc = CreateNPCFromAISettings( settings, team, origin, angles )
+ DispatchSpawn( npc )
+ }
+
+ if ( restoreHostThreadMode )
+ {
+ wait 0.2
+ RestorePrecacheErrors()
+ }
+}
+
+void function DEV_SpawnNPCWithWeaponAtCrosshair( string baseClass, string aiSettings, int team, string weaponName = "" )
+{
+ printt( "script thread DEV_SpawnNPCWithWeaponAtCrosshair( \"" + baseClass + "\", \"" + aiSettings + "\", " + team + ", \"" + weaponName + "\")" )
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+ bool restoreHostThreadMode = GetConVarInt( "host_thread_mode" ) != 0
+ entity npc = DEV_SpawnNPCWithWeaponAtCrosshairStart( restoreHostThreadMode, baseClass, aiSettings, team, weaponName )
+ DispatchSpawn( npc )
+ DEV_SpawnNPCWithWeaponAtCrosshairEnd( restoreHostThreadMode )
+}
+
+void function DEV_SpawnMercTitanAtCrosshair( string mercName )
+{
+ printt( "script thread DEV_SpawnMercTitanAtCrosshair( \"" + mercName + "\")" )
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+ TitanLoadoutDef ornull loadout = GetTitanLoadoutForBossCharacter( mercName )
+ if ( loadout == null )
+ return
+ expect TitanLoadoutDef( loadout )
+ string baseClass = "npc_titan"
+ string aiSettings = GetNPCSettingsFileForTitanPlayerSetFile( loadout.setFile )
+
+ bool restoreHostThreadMode = GetConVarInt( "host_thread_mode" ) != 0
+ entity npc = DEV_SpawnNPCWithWeaponAtCrosshairStart( restoreHostThreadMode, baseClass, aiSettings, TEAM_IMC )
+ SetSpawnOption_NPCTitan( npc, TITAN_MERC )
+ SetSpawnOption_TitanLoadout( npc, loadout )
+ npc.ai.bossTitanPlayIntro = false
+
+ DispatchSpawn( npc )
+ DEV_SpawnNPCWithWeaponAtCrosshairEnd( restoreHostThreadMode )
+}
+
+void function DEV_SpawnWeaponAtCrosshair( string weaponName )
+{
+ printt( "script thread DEV_SpawnWeaponAtCrosshair( \"" + weaponName + "\")" )
+
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+
+ entity player = GetPlayerArray()[ 0 ]
+ if ( !IsValid( player ) )
+ return
+ vector origin = GetPlayerCrosshairOrigin( player )
+ vector angles = Vector( 0, 0, 0 )
+ entity weapon = CreateWeaponEntityByNameWithPhysics( weaponName, origin, angles )
+
+#if SP
+ bool isTitanWeapon = weaponName.find( "mp_titanweapon_" ) != null
+ if ( isTitanWeapon )
+ thread TitanLoadoutWaitsForPickup( weapon, SPTitanLoadoutPickup )
+#endif
+
+}
+
+string function GetAISettingsFromPlayerSetFile( string playerSetfile )
+{
+ TitanLoadoutDef ornull loadout = GetTitanLoadoutForColumn( "setFile", playerSetfile )
+ Assert( loadout != null, "Couldn't find loadout with set file " + playerSetfile )
+ expect TitanLoadoutDef( loadout )
+
+ return expect string( Dev_GetPlayerSettingByKeyField_Global( playerSetfile, GetAISettingsStringForMode() ) )
+}
+
+
+void function DEV_SpawnBossTitanAtCrosshair( string playerSetfile )
+{
+ string aiSettings = GetAISettingsFromPlayerSetFile( playerSetfile )
+ printt( "script thread DEV_SpawnBossTitanAtCrosshair( \"" + aiSettings + "\")" )
+ Assert( IsNewThread(), "Must be threaded off due to precache issues" )
+
+ string baseClass = "npc_titan"
+ bool restoreHostThreadMode = GetConVarInt( "host_thread_mode" ) != 0
+ entity npc = DEV_SpawnNPCWithWeaponAtCrosshairStart( restoreHostThreadMode, baseClass, aiSettings, TEAM_IMC )
+ SetSpawnOption_NPCTitan( npc, TITAN_BOSS )
+// SetSpawnOption_TitanLoadout( npc, loadout )
+
+ string builtInLoadout = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+// SetTitanSettings( npc.ai.titanSettings, builtInLoadout )
+ npc.ai.titanSpawnLoadout.setFile = builtInLoadout
+ OverwriteLoadoutWithDefaultsForSetFile( npc.ai.titanSpawnLoadout ) // get the entire loadout, including defensive and tactical
+
+ DispatchSpawn( npc )
+ DEV_SpawnNPCWithWeaponAtCrosshairEnd( restoreHostThreadMode )
+}
+
+entity function DEV_SpawnNPCWithWeaponAtCrosshairStart( bool restoreHostThreadMode, string baseClass, string aiSettings, int team, string weaponName = "" )
+{
+ if ( restoreHostThreadMode )
+ {
+ DisablePrecacheErrors()
+ wait 0.5
+ }
+
+ float time = Time()
+ for ( ;; )
+ {
+ if ( Time() > time )
+ break
+ WaitFrame()
+ }
+ entity player = GetPlayerArray()[ 0 ]
+ if ( !IsValid( player ) )
+ return
+
+ vector origin = GetPlayerCrosshairOrigin( player )
+ vector angles = Vector( 0, 0, 0 )
+
+ entity npc = CreateNPC( baseClass, team, origin, angles )
+ if ( IsTurret( npc ) )
+ npc.kv.origin -= Vector( 0, 0, CROSSHAIR_VERT_OFFSET )
+ SetSpawnOption_AISettings( npc, aiSettings )
+
+ if ( npc.GetClassName() == "npc_soldier" || npc.GetClassName() == "npc_spectre" )
+ npc.kv.squadname = "crosshairSpawnSquad_team_" + team + "_" + baseClass + "_" + aiSettings
+
+ if ( weaponName != "" )
+ SetSpawnOption_Weapon( npc, weaponName )
+
+ if ( npc.GetClassName() == "npc_titan" )
+ {
+ string builtInLoadout = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+ SetTitanSettings( npc.ai.titanSettings, builtInLoadout )
+ npc.ai.titanSpawnLoadout.setFile = builtInLoadout
+ OverwriteLoadoutWithDefaultsForSetFile( npc.ai.titanSpawnLoadout ) // get the entire loadout, including defensive and tactical
+ }
+
+ return npc
+}
+
+void function DEV_SpawnNPCWithWeaponAtCrosshairEnd( bool restoreHostThreadMode )
+{
+ if ( restoreHostThreadMode )
+ {
+ wait 0.2
+ RestorePrecacheErrors()
+ }
+}
+
+
+function SetAISettingsWrapper( entity npc, string settings )
+{
+ npc.SetAISettings( settings )
+ Assert( settings.find( npc.GetClassName() ) == 0, "NPC classname " + npc.GetClassName() + " not found in " + settings )
+
+ if ( IsSingleplayer() )
+ {
+ FixupTitle( npc )
+ }
+}
+
+bool function WithinEngagementRange( entity npc, vector origin )
+{
+ entity weapon = npc.GetActiveWeapon()
+ if ( weapon == null )
+ return false
+
+ float dist = Distance( npc.GetOrigin(), origin )
+ if ( dist < weapon.GetWeaponInfoFileKeyField( "npc_min_engage_range" ) )
+ return false
+
+ return dist <= weapon.GetWeaponInfoFileKeyField( "npc_max_engage_range" )
+}
+
+
+function DEV_AITitanDuel()
+{
+ thread DEV_AITitanDuelThread()
+}
+
+entity function DEV_AITitanDuelSpawn( entity player, int team, vector origin, vector angles, aiSetting )
+{
+ entity titan = CreateNPC( "npc_titan", team, origin, angles )
+ SetSpawnOption_AISettings( titan, aiSetting )
+ DispatchSpawn( titan )
+
+ vector ornull clampedPos = NavMesh_ClampPointForAI( origin, titan )
+ if ( clampedPos != null )
+ {
+ titan.SetOrigin( expect vector( clampedPos ) )
+ }
+ else
+ {
+ array<entity> spawnpoints = SpawnPoints_GetTitan()
+ if ( spawnpoints.len() )
+ {
+ entity spawnpoint = GetClosest( spawnpoints, origin )
+ titan.SetOrigin( spawnpoint.GetOrigin() )
+ }
+ }
+
+ return titan
+}
+
+function DEV_AITitanDuelThread()
+{
+ DisablePrecacheErrors()
+ wait 0.5
+
+ array<string> aiSettings = GetAllowedTitanAISettings()
+
+ aiSettings.randomize()
+
+ entity player = GetPlayerArray()[ 0 ]
+
+ entity imcTitan = null
+ entity militiaTitan = null
+
+ int currentSetting = 0
+
+
+ while ( 1 )
+ {
+ if ( !IsValid( imcTitan ) )
+ {
+ vector origin = GetPlayerCrosshairOrigin( player ) + < -300, -300, 0 >
+ vector angles = Vector( 0, 0, 0 )
+
+ imcTitan = DEV_AITitanDuelSpawn( player, TEAM_IMC, origin, angles, aiSettings[currentSetting] )
+ currentSetting = (currentSetting + 1) % aiSettings.len()
+
+ if ( IsValid( militiaTitan ) )
+ {
+ imcTitan.SetEnemyLKP( militiaTitan, militiaTitan.GetOrigin() )
+ militiaTitan.SetEnemyLKP( imcTitan, imcTitan.GetOrigin() )
+ }
+ }
+
+ if ( !IsValid( militiaTitan ) )
+ {
+ vector origin = GetPlayerCrosshairOrigin( player ) + < 300, 300, 0 >
+ vector angles = Vector( 0, 180, 0 )
+
+ militiaTitan = DEV_AITitanDuelSpawn( player, TEAM_MILITIA, origin, angles, aiSettings[currentSetting] )
+ currentSetting = (currentSetting + 1) % aiSettings.len()
+
+ if ( IsValid( imcTitan ) )
+ {
+ militiaTitan.SetEnemyLKP( imcTitan, imcTitan.GetOrigin() )
+ imcTitan.SetEnemyLKP( militiaTitan, militiaTitan.GetOrigin() )
+ }
+ }
+
+ wait 2
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod.gnut
new file mode 100644
index 000000000..40a7d9328
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod.gnut
@@ -0,0 +1,187 @@
+untyped
+
+global function DropPod_Init
+
+global function CreateDropPod
+global function LaunchAnimDropPod
+global function GetDropPodAnimDuration
+global function CreateDropPodSmokeTrail
+
+const DP_COLL_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam_collision.mdl"
+const DROPPOD_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam.mdl"
+
+function DropPod_Init()
+{
+ PrecacheModel( DROPPOD_MODEL )
+
+ RegisterSignal( "OnLaunch" )
+ RegisterSignal( "OnImpact" )
+
+ PrecacheModel( DP_COLL_MODEL )
+
+ PrecacheEffect( $"droppod_trail" )
+ PrecacheEffect( $"droppod_impact" )
+}
+
+
+function GetDropPodAnimDuration()
+{
+ // hack seems bad to spawn an ent to get this info
+ entity dropPod = CreateDropPod()
+
+ local animDuration = dropPod.GetSequenceDuration( "pod_testpath" )
+ dropPod.Destroy()
+
+ return animDuration
+}
+
+function LaunchAnimDropPod( entity dropPod, string anim, vector targetOrigin, vector targetAngles )
+{
+ dropPod.EndSignal( "OnDestroy" )
+ dropPod.EnableRenderAlways()
+
+ dropPod.s.launchAnim <- anim
+
+ int team = dropPod.GetTeam()
+
+ entity ref = CreateOwnedScriptMover( dropPod )
+ ref.SetOrigin( targetOrigin )
+ ref.SetAngles( targetAngles )
+
+ OnThreadEnd(
+ function () : ( dropPod, ref )
+ {
+ if ( IsValid( dropPod ) )
+ {
+ dropPod.ClearParent()
+ }
+
+ if ( IsValid( ref ) )
+ ref.Kill_Deprecated_UseDestroyInstead()
+ }
+ )
+
+ local e = {}
+ e.targetOrigin <- targetOrigin
+ e.targetAngles <- targetAngles
+
+ AddAnimEvent( dropPod, "OnImpact", DropPodOnImpactFXAndShake, e )
+ EmitSoundOnEntity( dropPod, "spectre_drop_pod" )
+
+ FirstPersonSequenceStruct sequence
+ sequence.thirdPersonAnim = anim
+
+ sequence.blendTime = 0.0
+ sequence.attachment = "ref"
+ sequence.useAnimatedRefAttachment = true
+ //DrawArrow( ref.GetOrigin(), ref.GetAngles(), 5, 100 )
+ waitthread FirstPersonSequence( sequence, dropPod, ref )
+ dropPod.DisableRenderAlways()
+// WaitFrame()
+}
+
+function CheckPlayersIntersectingPod( pod, targetOrigin )
+{
+ array<entity> playerList = GetPlayerArray()
+
+ // Multiplying the bounds by 1.42 to ensure this encloses the droppod when it's rotated 45 degrees
+ local mins = pod.GetBoundingMins() * 1.42 + targetOrigin
+ local maxs = pod.GetBoundingMaxs() * 1.42 + targetOrigin
+ local safeRadiusSqr = 250 * 250
+
+ foreach ( player in playerList )
+ {
+ local playerOrigin = player.GetOrigin()
+
+ if ( DistanceSqr( targetOrigin, playerOrigin ) > safeRadiusSqr )
+ continue
+
+ local playerMins = player.GetBoundingMins() + playerOrigin
+ local playerMaxs = player.GetBoundingMaxs() + playerOrigin
+
+ if ( BoxIntersectsBox( mins, maxs, playerMins, playerMaxs ) )
+ return true
+ }
+
+ return false
+}
+
+entity function CreateDropPod( vector ornull origin = null, vector ornull angles = null )
+{
+ entity prop_dynamic = CreateEntity( "prop_dynamic" )
+ prop_dynamic.SetValueForModelKey( DROPPOD_MODEL )
+ prop_dynamic.kv.contents = int( prop_dynamic.kv.contents ) & ~CONTENTS_TITANCLIP
+ prop_dynamic.kv.fadedist = -1
+ prop_dynamic.kv.renderamt = 255
+ prop_dynamic.kv.rendercolor = "255 255 255"
+ prop_dynamic.kv.solid = 6 // 0 = no collision, 2 = bounding box, 6 = use vPhysics, 8 = hitboxes only
+ if ( origin )
+ {
+ prop_dynamic.SetOrigin( expect vector( origin ) )
+ if ( angles )
+ prop_dynamic.SetAngles( expect vector( angles ) )
+ }
+ DispatchSpawn( prop_dynamic )
+
+ return prop_dynamic
+}
+
+void function PushPlayerAndCreateDropPodCollision( entity pod, vector targetOrigin )
+{
+ pod.EndSignal( "OnDestroy" )
+
+ entity point_push = CreateEntity( "point_push" )
+ point_push.kv.spawnflags = 8
+ point_push.kv.enabled = 1
+ point_push.kv.magnitude = 140.0 * 0.75 //Compensate for reduced player gravity to match R1
+ point_push.kv.radius = 192.0
+ point_push.SetOrigin( targetOrigin + Vector( 0.0, 0.0, 32.0 ) )
+ DispatchSpawn( point_push )
+
+ OnThreadEnd(
+ function() : ( point_push )
+ {
+ point_push.Fire( "Kill", "", 0.0 )
+ }
+ )
+
+ while ( CheckPlayersIntersectingPod( pod, targetOrigin ) )
+ wait( 0.1 )
+
+ pod.Solid()
+}
+
+function DropPodOnImpactFX( droppod, e )
+{
+ PlayImpactFXTable( expect vector( e.targetOrigin ), expect entity( droppod ), HOTDROP_IMPACT_FX_TABLE )
+}
+
+void function DropPodOnImpactFXAndShake( entity droppod )
+{
+ var e = GetOptionalAnimEventVar( droppod, "OnImpact" )
+ DropPodOnImpactFX( droppod, e )
+ CreateShake( expect vector( e.targetOrigin ), 7, 0.15, 1.75, 768 )
+
+ // 1 - No Damage - Only Force
+ // 2 - Push players
+ // 8 - Test LOS before pushing
+ local flags = 11
+ local impactOrigin = e.targetOrigin + Vector( 0,0,10 )
+ local impactRadius = 192
+ thread PushPlayerAndCreateDropPodCollision( droppod, expect vector( e.targetOrigin ) )
+}
+
+
+function CreateDropPodSmokeTrail( pod )
+{
+ entity smokeTrail = CreateEntity( "info_particle_system" )
+ smokeTrail.SetValueForEffectNameKey( $"droppod_trail" )
+ smokeTrail.kv.start_active = 0
+ DispatchSpawn( smokeTrail )
+
+ smokeTrail.SetOrigin( pod.GetOrigin() + Vector( 0, 0, 152 ) )
+ smokeTrail.SetParent( pod )
+
+ return smokeTrail
+}
+
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod_fireteam.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod_fireteam.gnut
new file mode 100644
index 000000000..b93631ac8
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_droppod_fireteam.gnut
@@ -0,0 +1,246 @@
+global function DropPodFireteam_Init
+
+global function InitFireteamDropPod
+global function ActivateFireteamDropPod
+global function DropPodActiveThink
+
+global function CreateDropPodDoor
+
+const DP_ARM_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam_arm.mdl"
+const DP_DOOR_MODEL = $"models/vehicle/droppod_fireteam/droppod_fireteam_door.mdl"
+
+global enum eDropPodFlag
+{
+ DISSOLVE_AFTER_DISEMBARKS = (1<<0)
+}
+
+struct DroppodStruct
+{
+ entity door
+ bool openDoor = false
+ int numGuys = 0
+ int flags = 0
+}
+
+struct
+{
+ table<entity, DroppodStruct> droppodTable
+}
+file
+
+void function DropPodFireteam_Init()
+{
+ RegisterSignal( "OpenDoor" )
+
+ PrecacheModel( DP_DOOR_MODEL )
+ PrecacheModel( DP_ARM_MODEL )
+}
+
+void function InitFireteamDropPod( entity pod, int flags = 0 )
+{
+ pod.NotSolid()
+
+ DroppodStruct droppodData
+ droppodData.flags = flags
+ droppodData.door = CreateDropPodDoor( pod )
+ file.droppodTable[ pod ] <- droppodData
+
+ pod.Anim_Play( "idle" )
+}
+
+void function ActivateFireteamDropPod( entity pod, array<entity> guys )
+{
+ DroppodStruct droppodData = file.droppodTable[ pod ]
+ droppodData.openDoor = true
+ pod.Signal( "OpenDoor" )
+
+ if ( guys.len() >= 1 )
+ {
+ SetAnim( guys[0], "drop_pod_exit_anim", "pt_dp_exit_a" )
+ SetAnim( guys[0], "drop_pod_idle_anim", "pt_dp_idle_a" )
+ }
+
+ if ( guys.len() >= 2 )
+ {
+ SetAnim( guys[1], "drop_pod_exit_anim", "pt_dp_exit_b" )
+ SetAnim( guys[1], "drop_pod_idle_anim", "pt_dp_idle_b" )
+ }
+
+ if ( guys.len() >= 3 )
+ {
+ SetAnim( guys[2], "drop_pod_exit_anim", "pt_dp_exit_c" )
+ SetAnim( guys[2], "drop_pod_idle_anim", "pt_dp_idle_c" )
+ }
+
+ if ( guys.len() >= 4 )
+ {
+ SetAnim( guys[3], "drop_pod_exit_anim", "pt_dp_exit_d" )
+ SetAnim( guys[3], "drop_pod_idle_anim", "pt_dp_idle_d" )
+ }
+
+ foreach ( guy in guys )
+ {
+ if ( IsAlive( guy ) )
+ {
+ guy.MakeVisible()
+ entity weapon = guy.GetActiveWeapon()
+ if ( IsValid( weapon ) )
+ weapon.MakeVisible()
+
+ thread GuyHangsInPod( guy, pod )
+ }
+ }
+
+ thread DropPodActiveThink( pod )
+}
+
+void function DropPodActiveThink( entity pod )
+{
+ DroppodStruct droppodData = file.droppodTable[ pod ]
+
+ OnThreadEnd(
+ function() : ( pod )
+ {
+ DroppodStruct droppodData = file.droppodTable[pod]
+ if ( droppodData.flags & eDropPodFlag.DISSOLVE_AFTER_DISEMBARKS )
+ CleanupFireteamPod( pod )
+ else
+ delaythread( 10 ) CleanupFireteamPod( pod )
+ }
+ )
+
+ pod.EndSignal( "OnDestroy" )
+
+ if ( DropPodDoorInGround( pod ) )
+ droppodData.door.Destroy()
+ else
+ DropPodOpenDoor( pod, droppodData.door )
+
+ while ( droppodData.numGuys )
+ WaitFrame()
+}
+
+bool function DropPodDoorInGround( entity pod )
+{
+ string attachment = "hatch"
+ int attachIndex = pod.LookupAttachment( attachment )
+ vector end = pod.GetAttachmentOrigin( attachIndex )
+
+ string originAttachment = "origin"
+ int originAttachIndex = pod.LookupAttachment( originAttachment )
+ vector start = pod.GetAttachmentOrigin( originAttachIndex )
+
+ TraceResults result = TraceLine( start, end, pod, TRACE_MASK_SOLID, TRACE_COLLISION_GROUP_NONE )
+
+ return result.fraction < 1.0
+}
+
+void function CleanupFireteamPod( entity pod )
+{
+ DroppodStruct droppodData = file.droppodTable[ pod ]
+
+ if ( !IsValid( pod ) )
+ return
+
+ if ( IsValid( droppodData.door ) )
+ droppodData.door.Dissolve( ENTITY_DISSOLVE_CORE, Vector( 0, 0, 0 ), 500 )
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, pod.GetOrigin(), "droppod_dissolve" )
+
+ delete file.droppodTable[ pod ]
+
+ pod.NotSolid()
+ foreach( ent in pod.e.attachedEnts )
+ {
+ ent.NotSolid()
+ }
+ pod.Dissolve( ENTITY_DISSOLVE_CORE, Vector( 0, 0, 0 ), 500 )
+}
+
+entity function CreateDropPodDoor( entity pod )
+{
+ string attachment = "hatch"
+ int attachIndex = pod.LookupAttachment( attachment )
+ vector origin = pod.GetAttachmentOrigin( attachIndex )
+ vector angles = pod.GetAttachmentAngles( attachIndex )
+
+ entity prop_physics = CreateEntity( "prop_physics" )
+ SetTargetName( prop_physics, "door" + UniqueString() )
+ prop_physics.SetValueForModelKey( DP_DOOR_MODEL )
+ // Start Asleep
+ // Debris - Don't collide with the player or other debris
+ // Generate output on +USE
+ prop_physics.kv.spawnflags = 261 // non solid for now
+ prop_physics.kv.fadedist = -1
+ prop_physics.kv.physdamagescale = 0.1
+ prop_physics.kv.inertiaScale = 1.0
+ prop_physics.kv.renderamt = 0
+ prop_physics.kv.rendercolor = "255 255 255"
+
+ DispatchSpawn( prop_physics )
+
+ prop_physics.SetOrigin( origin )
+ prop_physics.SetAngles( angles )
+ prop_physics.SetParent( pod, "HATCH", false )
+ prop_physics.MarkAsNonMovingAttachment()
+
+ return prop_physics
+}
+
+void function DropPodOpenDoor( entity pod, entity door )
+{
+ door.ClearParent()
+ door.SetVelocity( door.GetForwardVector() * 500 )
+ EmitSoundOnEntity( pod, "droppod_door_open" )
+}
+
+void function GuyHangsInPod( entity guy, entity pod )
+{
+ DroppodStruct droppodData = file.droppodTable[ pod ]
+
+ guy.EndSignal( "OnDeath" )
+ guy.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( droppodData )
+ {
+ droppodData.numGuys--
+ }
+ )
+
+ droppodData.numGuys++
+
+ string idleAnim
+ string exitAnim
+
+ if ( !droppodData.openDoor )
+ {
+ guy.SetEfficientMode( true )
+
+ guy.SetParent( pod, "ATTACH", false )
+
+ idleAnim = expect string( GetAnim( guy, "drop_pod_idle_anim" ) )
+ if ( guy.LookupSequence( idleAnim ) != -1 )
+ guy.Anim_ScriptedPlay( idleAnim )
+
+ pod.WaitSignal( "OpenDoor" )
+
+ //wait POST_TURRET_DELAY
+
+ guy.SetEfficientMode( false )
+ }
+
+
+ guy.SetParent( pod, "ATTACH", false )
+
+ exitAnim = expect string ( GetAnim( guy, "drop_pod_exit_anim" ) )
+ bool exitAnimExists = guy.LookupSequence( exitAnim ) != -1
+ if ( exitAnimExists )
+ guy.Anim_ScriptedPlay( exitAnim )
+
+ guy.ClearParent()
+
+ if ( exitAnimExists )
+ WaittillAnimDone( guy )
+ guy.Signal( "npc_deployed" )
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_grunt_chatter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_grunt_chatter.gnut
new file mode 100644
index 000000000..f5c0c84d9
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_grunt_chatter.gnut
@@ -0,0 +1,1786 @@
+// _grunt_chatter.gnut
+
+global function GruntChatter_Init
+global function GruntChatter_AddCategory
+global function GruntChatter_AddEvent
+global function GruntChatter_TryCloakedPilotSpotted
+global function GruntChatter_TryThrowingGrenade
+global function GruntChatter_TryFriendlyEquipmentDeployed
+global function GruntChatter_TryPersonalShieldDamaged
+global function GruntChatter_TryDisplacingFromDangerousArea
+global function GruntChatter_TryEnemyTimeShifted
+global function GruntChatter_TryIncomingSpawn
+global function GruntChatter_TryPlayerPilotReloading
+global function GruntChatter_TryGruntFlankedByPlayer
+
+const float CHATTER_THINK_WAIT = 1.0
+const float CHATTER_SIGNAL_INTERRUPT_WAIT = 1.0 // how often the grunts will interrupt their signal waiting thread to check their kv timers
+const float CHATTER_EVENT_EXPIRE_TIME = 3.0 // chatter events get thrown away when they're at least this old
+
+const float CHATTER_PLAYER_COMBAT_STATE_CHANGE_DEBOUNCE = 1.5
+
+const float CHATTER_PILOT_LOST_NEARBY_TEAMMATE_DIST = 1024.0
+const float CHATTER_PLAYER_CLOSE_MIN_DIST = 370.0 // all squad members have to be at least this far away from enemy to say they lost visual
+
+const float CHATTER_PILOT_SPOTTED_CLOSE_DIST = 600.0
+const float CHATTER_PILOT_SPOTTED_MID_DIST = 1100.0
+const float CHATTER_PILOT_SPOTTED_NEARBY_TEAMMATE_DIST = 1024.0
+
+const float CHATTER_PILOT_SPOTTED_MID_DIST_MOVING_MIN_SPEED = 170.0
+
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_MIN = 600.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_MAX = 1400.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_20 = 787.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_25 = 984.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_30 = 1181.0
+const float CHATTER_PILOT_SPOTTED_RANGE_DIST_35 = 1378.0
+
+const float CHATTER_PILOT_DECOY_SPOTTED_DIST_MAX = 1500.0
+
+const float CHATTER_ENEMY_GRUNT_SPOTTED_DIST = 1250.0
+const float CHATTER_ENEMY_TITAN_SPOTTED_DIST = 3000.0
+const float CHATTER_ENEMY_TITAN_SPOTTED_DIST_CLOSE = 1024.0
+const float CHATTER_ENEMY_SPECTRE_SPOTTED_DIST = 1250.0
+const float CHATTER_ENEMY_SPECTRE_SPOTTED_DIST_CLOSE = 650.0
+const float CHATTER_ENEMY_TICK_SPOTTED_DIST = 1024.0
+
+const float CHATTER_PILOT_SPOTTED_ABOVE_DIST_MIN = 128.0
+const float CHATTER_PILOT_SPOTTED_ABOVE_DIST_MAX = 1024.0
+const float CHATTER_PILOT_SPOTTED_ABOVE_RADIUS = 450.0
+const float CHATTER_PILOT_SPOTTED_BELOW_DIST_MIN = 128.0
+const float CHATTER_PILOT_SPOTTED_BELOW_DIST_MAX = 1024.0
+const float CHATTER_PILOT_SPOTTED_BELOW_RADIUS = 512.0
+
+const float CHATTER_GRUNT_ENEMY_OUT_OF_SIGHT_TIME = 15.0
+
+const float CHATTER_FRIENDLY_EQUIPMENT_DEPLOYED_NEARBY_DIST = 900.0 // distance from the Specialist that a Grunt will chatter about him deploying things
+
+const bool CHATTER_DO_UNSUSPECTING_PILOT_CALLOUTS = false // couldn't get it working well enough in time just in script... next game maybe
+const float CHATTER_UNSUSPECTING_PILOT_TARGET_DIST_MAX = 512.0
+const float CHATTER_UNSUSPECTING_PILOT_TARGET_MIN_DOT_REAR = 0.65
+const float CHATTER_UNSUSPECTING_PILOT_MAX_SPEED = 170.0 // player has to be below this speed to trigger "unsuspecting pilot"
+const float CHATTER_UNSUSPECTING_PILOT_STATETIME_MIN = 2.0 // how long the player has to be in "unsuspecting state" before we try to chatter about it
+
+const float CHATTER_SEE_CLOAKED_PILOT_MIN_DOT_REAR = 0.65
+
+const float CHATTER_SUPPRESSION_EXPIRE_TIME = 0.2 // secs after kv.lastSuppressionTime that we will be ok with adding a chatter event about it
+const float CHATTER_MISS_FAST_TARGET_EXPIRE_TIME = 0.5 // secs after kv.lastMissFastPlayerTime that we will be ok with adding a chatter event about it
+const float CHATTER_MISS_FAST_TARGET_MIN_SPEED = 350.0 // min "speed" that player needs to be moving to trigger a missing fast player callout
+
+const float CHATTER_PILOT_LOW_HEALTH_FRAC = 0.35 // below this fraction of pilot maxhealth, enemies can chatter about pilot low health
+const float CHATTER_PILOT_LOW_HEALTH_RANGE = 1024.0 // beyond this distance, enemies won't chatter about pilot low health
+const float CHATTER_PLAYER_RELOADING_RANGE = 800.0
+
+const float CHATTER_NEARBY_GRUNT_TRACEFRAC_MIN = 0.95 // for when we need "LOS" trace
+
+const float CHATTER_ENEMY_PILOT_MULTIKILL_EXPIRETIME = 4.5 // max time between kills to trigger multikill callout
+const int CHATTER_PILOT_MULTIKILL_MIN_KILLS = 3
+
+const float CHATTER_FRIENDLY_GRUNT_DOWN_DIST_MAX = 1100.0
+const float CHATTER_FRIENDLY_TITAN_DOWN_DIST_MAX = 1500.0
+const float CHATTER_ENEMY_PILOT_DOWN_DIST_MAX = 600.0
+const float CHATTER_ENEMY_GRUNT_DOWN_DIST_MAX = 800.0
+const float CHATTER_ENEMY_TITAN_DOWN_DIST_MAX = 1500.0
+const float CHATTER_ENEMY_SPECTRE_DOWN_DIST_MAX = 800.0
+
+const float CHATTER_NEARBY_TITAN_DIST = 1024.0
+const float CHATTER_NEARBY_REAPER_DIST = 1024.0
+const float CHATTER_NEARBY_SPECTRE_DIST = 800.0
+
+const float CHATTER_ENEMY_TIME_SHIFT_NEARBY_DIST = 700.0
+
+const float CHATTER_SQUAD_DEPLETED_FRIENDLY_NEARBY_DIST = 650.0 // if any other friendly grunt is within this dist, squad deplete chatter won't play
+
+const float CHATTER_DANGEROUS_AREA_NEARBY_RANGE = 512.0
+
+struct ChatterCategory
+{
+ string alias
+ int priority = -1
+ string timer
+ string secondaryTimer
+ bool trackEventTarget
+ bool resetTargetKillChain
+}
+
+struct ChatterEvent
+{
+ ChatterCategory& category
+ entity npc = null
+ bool hasNPC = false
+ entity target = null
+ bool hasTarget = false
+ bool isValid = false
+ float time = -1
+}
+
+struct
+{
+ array<ChatterEvent> chatterEvents = []
+ table< string, ChatterCategory > chatterCategories
+ int usedEventTargetsArrayHandle
+
+ int pilotKillChainCounter = 0
+ float lastPilotKillTime = -1
+
+ int debugLevel = 0
+} file
+
+void function GruntChatter_Init()
+{
+ Assert( IsSingleplayer(), "Grunt chatter is only set up for SP." )
+
+ AddSpawnCallback( "player", GruntChatter_OnPlayerSpawned )
+ AddSpawnCallback( "npc_soldier", GruntChatter_OnGruntSpawned )
+ AddSpawnCallback( "npc_turret_sentry", GruntChatter_OnSentryTurretSpawned )
+
+ RegisterSignal( "GruntChatter_CombatStateChangeThread" )
+ RegisterSignal( "GruntChatter_Interrupt" )
+
+ file.usedEventTargetsArrayHandle = CreateScriptManagedEntArray()
+
+ AddCallback_OnPlayerKilled( GruntChatter_OnPlayerOrNPCKilled )
+ AddCallback_OnNPCKilled( GruntChatter_OnPlayerOrNPCKilled )
+ AddDeathCallback( "player_decoy", GruntChatter_OnPilotDecoyKilled )
+
+ GruntChatter_SharedInit()
+}
+
+void function GruntChatter_OnPlayerSpawned( entity player )
+{
+ thread GruntChatter_PlayerThink( player )
+ thread GruntChatter_TrackGruntCombatStateVsPlayer( player )
+
+ if ( CHATTER_DO_UNSUSPECTING_PILOT_CALLOUTS )
+ thread GruntChatter_DetectPlayerPilotUnsuspecting( player )
+}
+
+void function GruntChatter_OnGruntSpawned( entity grunt )
+{
+ if( IsMultiplayer() )
+ return
+
+ if ( !GruntChatter_IsGruntTypeEligibleForChatter( grunt ) )
+ return
+
+ AddEntityCallback_OnDamaged( grunt, GruntChatter_OnGruntDamaged )
+
+ thread GruntChatter_GruntSignalWait( grunt )
+}
+
+void function GruntChatter_OnSentryTurretSpawned( entity turret )
+{
+ if ( turret.GetTeam() != TEAM_IMC )
+ return
+
+ thread GruntChatter_TurretSignalWait( turret )
+}
+
+// ==== chatter mission control ====
+void function GruntChatter_AddCategory( string chatterAlias, int priority, string timerAlias, string secondaryTimerAlias, bool trackEventTarget, bool resetTargetKillChain )
+{
+ Assert( !( chatterAlias in file.chatterCategories ), "Chatter alias already set up: " + chatterAlias )
+ Assert( TimerExists( timerAlias ), "Grunt chatter timer not set up in grunt_chatter_timers.csv: " + timerAlias )
+
+ ChatterCategory newCategory
+ newCategory.alias = chatterAlias
+ newCategory.priority = priority
+ newCategory.timer = timerAlias
+ newCategory.trackEventTarget = trackEventTarget
+ newCategory.resetTargetKillChain = resetTargetKillChain
+
+ if ( secondaryTimerAlias != "" )
+ newCategory.secondaryTimer = secondaryTimerAlias
+
+ file.chatterCategories[ chatterAlias ] <- newCategory
+}
+
+// add a grunt to have him chatter specifically
+// target: must be alive or else event won't fire
+void function GruntChatter_AddEvent( string alias, entity ornull npc = null, entity ornull target = null )
+{
+ Assert( alias in file.chatterCategories, "Couldn't find chatter category alias " + alias + ", was it set up?" )
+
+ ChatterEvent newEvent
+ newEvent.category = file.chatterCategories[ alias ]
+ newEvent.isValid = true
+ newEvent.time = Time()
+
+ if ( npc != null )
+ {
+ newEvent.npc = expect entity( npc )
+ newEvent.hasNPC = true
+ }
+
+ if ( file.chatterCategories[ alias ].trackEventTarget )
+ Assert( target != null, "Category " + file.chatterCategories[ alias ].alias + " requires a target to track for its events." )
+
+ if ( file.chatterCategories[ alias ].resetTargetKillChain )
+ Assert( target != null, "Category " + file.chatterCategories[ alias ].alias + " requires a target on which to record kill chains." )
+
+ if ( target != null )
+ {
+ newEvent.target = expect entity( target )
+ newEvent.hasTarget = true
+ }
+
+ if ( file.debugLevel > 1 )
+ printt( "ADDING EVENT:", newEvent.category.alias )
+
+ file.chatterEvents.append( newEvent )
+}
+
+void function GruntChatter_AddToUsedEventTargets( entity ent )
+{
+ Assert( !GruntChatter_EventTargetAlreadyUsed( ent ), "Ent already added to event targets: " + ent )
+ AddToScriptManagedEntArray( file.usedEventTargetsArrayHandle, ent )
+}
+
+bool function GruntChatter_EventTargetAlreadyUsed( entity ent )
+{
+ return ScriptManagedEntArrayContains( file.usedEventTargetsArrayHandle, ent )
+}
+
+void function GruntChatter_PlayerThink( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ while ( 1 )
+ {
+ wait CHATTER_THINK_WAIT
+
+ // squad conversations don't play to dead players
+ if ( !IsAlive( player ) )
+ continue
+
+ if ( player.GetForcedDialogueOnly() )
+ continue
+
+ if ( !file.chatterEvents.len() )
+ continue
+
+ if ( !TimerCheck( "chatter_global" ) )
+ continue
+
+ // prune expired chatter events if necessary
+ GruntChatter_RemoveExpiredEventsFromQueue()
+
+ // process chatter events
+ array< ChatterEvent > currChatterEvents = file.chatterEvents
+
+ ChatterEvent eventToPlay
+
+ foreach ( chatterEvent in currChatterEvents )
+ {
+ // check timer
+ if ( !TimerCheck( chatterEvent.category.timer ) )
+ continue
+
+ // check priority vs currently selected
+ if ( chatterEvent.category.priority < eventToPlay.category.priority )
+ continue
+
+ // check ents are still legit
+ if ( chatterEvent.hasNPC )
+ {
+ if ( !GruntChatter_CanGruntChatterNow( chatterEvent.npc ) )
+ continue
+
+ if ( !GruntChatter_CanGruntChatterToPlayer( chatterEvent.npc, player ) )
+ continue
+ }
+
+ if ( chatterEvent.hasTarget && !GruntChatter_CanChatterEventUseEnemyTarget( chatterEvent ) )
+ continue
+
+ // check which event is more current
+ if ( eventToPlay.time > chatterEvent.time )
+ continue
+
+ eventToPlay = chatterEvent
+ }
+
+ if ( eventToPlay.isValid )
+ {
+ string alias = eventToPlay.category.alias
+ string timer = eventToPlay.category.timer
+
+ entity grunt = eventToPlay.npc
+ // if the event didn't include a grunt, use the closest grunt as the source
+ if ( !IsValid( grunt ) )
+ {
+ // only human grunts should talk
+ array<entity> nearbyGrunts = GetNearbyEnemyHumanGrunts( player.GetOrigin(), player.GetTeam() )
+
+ if ( !nearbyGrunts.len() )
+ {
+ if ( file.debugLevel > 0 )
+ printt( "GRUNT CHATTER: can't play chatter event because nobody is close enough:", alias )
+
+ continue
+ }
+
+ nearbyGrunts = ArrayClosest( nearbyGrunts, player.GetOrigin() )
+ grunt = nearbyGrunts[0]
+ }
+
+ Assert( IsAlive( grunt ), "Grunt chatter error: need a grunt to talk" )
+
+ if ( file.debugLevel > 0 )
+ printt( "GRUNT CHATTER:", alias )
+
+ if ( eventToPlay.category.trackEventTarget )
+ GruntChatter_AddToUsedEventTargets( eventToPlay.target )
+
+ if ( eventToPlay.category.resetTargetKillChain )
+ GruntChatter_ResetPilotKillChain( eventToPlay.target )
+
+ PlaySquadConversationToAll( alias, grunt )
+ ChatterTimerReset( eventToPlay )
+
+ // throw away all the old chatter events now that we processed one
+ GruntChatter_FlushEventQueue()
+ }
+ }
+}
+
+void function GruntChatter_FlushEventQueue()
+{
+ file.chatterEvents = []
+}
+
+void function GruntChatter_RemoveExpiredEventsFromQueue()
+{
+ array< ChatterEvent > recentEvents = []
+ foreach ( event in file.chatterEvents )
+ {
+ if ( Time() - event.time >= CHATTER_EVENT_EXPIRE_TIME )
+ {
+ if ( file.debugLevel > 1 )
+ printt( "expired event:", event.category.alias, "time:", Time() - event.time )
+
+ continue
+ }
+
+ recentEvents.append( event )
+ }
+
+ file.chatterEvents = recentEvents
+}
+
+void function ChatterTimerReset( ChatterEvent event )
+{
+ TimerReset( "chatter_global" )
+ TimerReset( event.category.timer )
+
+ if ( event.category.secondaryTimer != "" )
+ TimerReset( event.category.secondaryTimer )
+}
+
+
+// ==== combat state tracking ====
+void function GruntChatter_TrackGruntCombatStateVsPlayer( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ while ( 1 )
+ {
+ wait 1.0
+
+ if ( !IsAlive( player ) )
+ continue
+
+ int currState = GruntChatter_GetGruntCombatStateVsPlayer( player )
+
+ if ( currState == svGlobalSP.gruntCombatState )
+ continue
+
+ if ( file.debugLevel > 1 )
+ printt( "combat state change:", currState )
+
+ thread GruntChatter_TryPlayerPilotCombatStateChange( player, currState, svGlobalSP.gruntCombatState )
+
+ svGlobalSP.gruntCombatState = currState
+ }
+}
+
+int function GruntChatter_GetGruntCombatStateVsPlayer( entity player )
+{
+ array<entity> enemies = GetNPCArrayEx( "npc_soldier", TEAM_ANY, player.GetTeam(), Vector( 0, 0, 0 ), -1 )
+ ArrayRemoveDead( enemies )
+
+ int currState = eGruntCombatState.IDLE
+
+ foreach ( npc in enemies )
+ {
+ if ( !IsAlive( npc ) )
+ continue
+
+ if ( npc.GetNPCState() == "alert" && currState != eGruntCombatState.COMBAT )
+ currState = eGruntCombatState.ALERT
+ else if ( npc.GetNPCState() == "combat" && npc.GetEnemy() == player )
+ return eGruntCombatState.COMBAT
+ }
+
+ return currState
+}
+
+
+// ==== player event handling ====
+// not currently used - I can't make it work well enough in script. Maybe code next game.
+void function GruntChatter_DetectPlayerPilotUnsuspecting( entity player )
+{
+ player.EndSignal( "OnDestroy" )
+
+ bool resetUnsuspectingTime = true
+ float unsuspectingTime = -1
+ array<entity> nearbyGrunts
+
+ while ( 1 )
+ {
+ if ( resetUnsuspectingTime )
+ {
+ if ( Time() - unsuspectingTime >= CHATTER_UNSUSPECTING_PILOT_STATETIME_MIN )
+ if ( file.debugLevel > 2 )
+ printt( "========== RESET UNSUSPECTING!" )
+
+ unsuspectingTime = Time()
+ }
+
+ wait 1.0
+
+ if ( !IsAlive( player ) )
+ continue
+
+ if ( !IsPilot( player ) )
+ continue
+
+ if ( Length( player.GetVelocity() ) > CHATTER_UNSUSPECTING_PILOT_MAX_SPEED )
+ continue
+
+ array<entity> validGrunts
+
+ nearbyGrunts = GetNearbyEnemyHumanGrunts( player.GetOrigin(), player.GetTeam(), CHATTER_UNSUSPECTING_PILOT_TARGET_DIST_MAX )
+ if ( !nearbyGrunts.len() )
+ continue
+
+ foreach ( grunt in nearbyGrunts )
+ {
+ if ( grunt.GetEnemy() != player )
+ continue
+
+ // don't care about facing direction, just if he can trace to the player
+ if ( !GruntChatter_CanGruntTraceToLocation( grunt, player.EyePosition() ) )
+ continue
+
+ if ( !GruntChatter_IsTargetFacingAway( grunt, player, CHATTER_UNSUSPECTING_PILOT_TARGET_MIN_DOT_REAR ) )
+ continue
+
+ validGrunts.append( grunt )
+ }
+
+ if ( !validGrunts.len() )
+ continue
+
+ resetUnsuspectingTime = false
+
+ if ( file.debugLevel > 2 )
+ printt( "========== PLAYER IS UNSUSPECTING!" )
+
+ if ( unsuspectingTime < Time() && Time() - unsuspectingTime < CHATTER_UNSUSPECTING_PILOT_STATETIME_MIN )
+ continue
+
+ if ( !TimerCheck( "chatter_pilot_target_unsuspecting" ) )
+ {
+ if ( file.debugLevel > 2 )
+ printt( "waiting for UNSUSPECTING chatter timer...")
+
+ continue
+ }
+
+ entity closestGrunt = GetClosest( validGrunts, player.GetOrigin() )
+ GruntChatter_AddEvent( "gruntchatter_pilot_target_unsuspecting", closestGrunt, player )
+
+ resetUnsuspectingTime = true
+ }
+}
+
+
+// ==== grunt event handling ====
+void function GruntChatter_GruntSignalWait( entity grunt )
+{
+ grunt.EndSignal( "OnDeath" )
+ grunt.EndSignal( "OnDestroy" )
+
+ while ( 1 )
+ {
+ thread GruntChatter_InterruptSignal( grunt )
+ table result = WaitSignal( grunt, "OnFoundEnemy", "OnSeeEnemy", "OnLostEnemy", "GruntChatter_Interrupt" )
+
+ string signal = expect string( result.signal )
+
+ switch( signal )
+ {
+ // Sees target for the first time, or switches back to a target
+ case "OnFoundEnemy":
+ entity enemy = expect entity( result.value )
+ GruntChatter_TryOnFoundEnemy( grunt, enemy )
+ break
+
+ // Sees active target ent again
+ case "OnSeeEnemy":
+ entity enemy = expect entity( result.activator )
+ GruntChatter_TryPlayerPilotSpotted( grunt, enemy, signal )
+ break
+
+ // can no longer see active target ent
+ case "OnLostEnemy":
+ entity lostEnemy = expect entity( result.activator )
+ GruntChatter_TryPilotLost( grunt, lostEnemy )
+
+ // Grunt will send OnLost and OnFound at the same time if switching targets
+ entity newEnemy = grunt.GetEnemy()
+ if ( IsAlive( newEnemy ) )
+ GruntChatter_TryOnFoundEnemy( grunt, newEnemy )
+ break
+
+ case "GruntChatter_Interrupt":
+ GruntChatter_CheckGruntForEvents( grunt )
+ break
+ }
+ }
+}
+
+void function GruntChatter_TryOnFoundEnemy( entity grunt, entity enemy )
+{
+ GruntChatter_TryPlayerPilotSpotted( grunt, enemy, "OnFoundEnemy" )
+ GruntChatter_TryEnemySpotted( grunt, enemy )
+}
+
+void function GruntChatter_InterruptSignal( entity grunt )
+{
+ grunt.EndSignal( "OnDeath" )
+ grunt.EndSignal( "OnDestroy" )
+
+ grunt.EndSignal( "OnFoundEnemy" )
+ grunt.EndSignal( "OnSeeEnemy" )
+ grunt.EndSignal( "OnLostEnemy" )
+
+ wait CHATTER_SIGNAL_INTERRUPT_WAIT
+ grunt.Signal( "GruntChatter_Interrupt" )
+}
+
+// tries to send all valid events, lets the priority system handle which one should play
+void function GruntChatter_CheckGruntForEvents( entity grunt )
+{
+ GruntChatter_TryFriendlyPassingNearby( grunt )
+
+ // everything below this cares about having a living target
+ entity target = grunt.GetEnemy()
+ if ( !IsAlive( target ) )
+ return
+
+ GruntChatter_HACK_TryPilotTargetOutOfSight( grunt, target )
+ GruntChatter_TrySuppressingPilotTarget( grunt, target )
+ GruntChatter_TryMissingFastTarget( grunt, target )
+ GruntChatter_TryPilotLowHealth( grunt, target )
+ GruntChatter_TryEngagingNonPilotTarget( grunt, target )
+}
+
+// HACK fakey pilot lost if player out of sight for a while
+void function GruntChatter_HACK_TryPilotTargetOutOfSight( entity grunt, entity target )
+{
+ entity gruntEnemy = grunt.GetEnemy()
+
+ if ( !IsAlive( gruntEnemy ) )
+ return
+
+ if ( !IsPilot( gruntEnemy ) )
+ return
+
+ if ( grunt.GetNPCState() != "combat" )
+ return
+
+ if ( grunt.GetEnemyLastTimeSeen() == 0 )
+ return
+
+ if ( Time() - grunt.GetEnemyLastTimeSeen() < CHATTER_GRUNT_ENEMY_OUT_OF_SIGHT_TIME )
+ return
+
+ //if ( file.debugLevel > 1 )
+ // printt( "FAKEY LOST TARGET" )
+
+ if ( !TimerCheck( "chatter_pilot_lost" ) )
+ return
+
+ GruntChatter_TryPilotLost( grunt, gruntEnemy )
+}
+
+void function GruntChatter_TryPlayerPilotCombatStateChange( entity player, int currState, int prevState )
+{
+ // these lines are mostly written as if the state changes are happening during combat vs a Pilot
+ if ( !IsPilot( player ) )
+ return
+
+ player.Signal( "GruntChatter_CombatStateChangeThread" )
+ player.EndSignal( "GruntChatter_CombatStateChangeThread" )
+ player.EndSignal( "OnDeath" )
+
+ wait CHATTER_PLAYER_COMBAT_STATE_CHANGE_DEBOUNCE
+
+ string alias = ""
+ switch ( currState )
+ {
+ case eGruntCombatState.ALERT:
+ alias = "gruntchatter_statechange_idle2alert"
+ if ( prevState == eGruntCombatState.COMBAT )
+ alias = "gruntchatter_statechange_combat2alert"
+ break
+
+ case eGruntCombatState.COMBAT:
+ alias = "gruntchatter_statechange_idle2combat"
+ if ( prevState == eGruntCombatState.ALERT )
+ alias = "gruntchatter_statechange_alert2combat"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias )
+}
+
+void function GruntChatter_TryPilotLost( entity grunt, entity enemy )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( enemy ) || !IsPilot( enemy ) )
+ return
+
+ if ( !TimerCheck( "chatter_pilot_lost" ) )
+ return
+
+ // if anyone near you can see the enemy, don't say we lost the target
+ if ( CanNearbyGruntTeammatesSeeEnemy( grunt, enemy, CHATTER_PILOT_LOST_NEARBY_TEAMMATE_DIST ) )
+ return
+
+ // if a nearby friendly grunt is close to the enemy don't chatter about losing sight of the enemy
+ if ( GruntChatter_IsFriendlyGruntCloseToLocation( grunt.GetTeam(), enemy.GetOrigin(), CHATTER_PLAYER_CLOSE_MIN_DIST ) )
+ return
+
+ string alias = "gruntchatter_pilot_lost"
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( grunt.GetOrigin(), grunt.GetTeam(), CHATTER_PILOT_LOST_NEARBY_TEAMMATE_DIST )
+ if ( nearbyGrunts.len() >= 2 && RandomInt( 100 ) < 40 )
+ alias = "gruntchatter_pilot_lost_neg"
+
+ GruntChatter_AddEvent( alias, grunt )
+}
+
+void function GruntChatter_TryPlayerPilotSpotted( entity grunt, entity player, string resultSignal )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( player ) || !player.IsPlayer() || !IsPilot( player ) )
+ return
+
+ if ( TimerCheck ( "chatter_pilot_spotted" ) )
+ {
+ string spottedAlias = "gruntchatter_pilot_spotted"
+
+ if ( resultSignal == "OnFoundEnemy" )
+ {
+ if ( svGlobalSP.gruntCombatState != eGruntCombatState.COMBAT )
+ {
+ spottedAlias = "gruntchatter_pilot_first_sighting"
+ }
+ }
+ else
+ {
+ float distToPilot = Distance( grunt.GetOrigin(), player.GetOrigin() )
+ bool canSeePilot = grunt.CanSee( player )
+ bool pilotIsMoving = Length( player.GetVelocity() ) >= CHATTER_PILOT_SPOTTED_MID_DIST_MOVING_MIN_SPEED
+
+ if ( canSeePilot )
+ {
+ if ( distToPilot <= CHATTER_PILOT_SPOTTED_CLOSE_DIST )
+ {
+ spottedAlias = "gruntchatter_pilot_spotted_close_range"
+ }
+ else if ( canSeePilot && distToPilot > CHATTER_PILOT_SPOTTED_CLOSE_DIST && distToPilot <= CHATTER_PILOT_SPOTTED_MID_DIST )
+ {
+ spottedAlias = "gruntchatter_pilot_spotted_mid_range"
+ if ( pilotIsMoving )
+ spottedAlias = "gruntchatter_pilot_spotted_mid_range_moving"
+ }
+
+ if ( TimerCheck( "chatter_pilot_spotted_specific_range" ) && RandomInt( 100 ) < 40 )
+ {
+ table<string, float> rangeDists
+ rangeDists["chatter_pilot_spotted_specific_range_20"] <- CHATTER_PILOT_SPOTTED_RANGE_DIST_20
+ rangeDists["chatter_pilot_spotted_specific_range_25"] <- CHATTER_PILOT_SPOTTED_RANGE_DIST_25
+ rangeDists["chatter_pilot_spotted_specific_range_30"] <- CHATTER_PILOT_SPOTTED_RANGE_DIST_30
+ rangeDists["chatter_pilot_spotted_specific_range_35"] <- CHATTER_PILOT_SPOTTED_RANGE_DIST_35
+
+ if ( distToPilot >= CHATTER_PILOT_SPOTTED_RANGE_DIST_MIN && distToPilot <= CHATTER_PILOT_SPOTTED_RANGE_DIST_MAX )
+ {
+ string closestAlias
+ float closestDist
+ foreach ( rangeAlias, rangeDist in rangeDists )
+ {
+ float thisDist = fabs( distToPilot - rangeDist )
+ if ( closestAlias == "" || thisDist < closestDist )
+ {
+ closestAlias = rangeAlias
+ closestDist = thisDist
+ }
+ }
+
+ spottedAlias = closestAlias
+ }
+ }
+ }
+ }
+
+ GruntChatter_AddEvent( spottedAlias, grunt )
+ }
+
+ if ( TimerCheck ( "chatter_pilot_spotted_abovebelow" ) )
+ {
+ bool isEnemyAbove = GruntChatter_IsEnemyAbove( grunt, player )
+ bool isEnemyBelow = GruntChatter_IsEnemyBelow( grunt, player )
+
+ if ( isEnemyAbove )
+ GruntChatter_AddEvent( "gruntchatter_pilot_spotted_above", grunt )
+ else if ( isEnemyBelow )
+ GruntChatter_AddEvent( "gruntchatter_pilot_spotted_below", grunt )
+ }
+}
+
+void function GruntChatter_TryEnemySpotted( entity grunt, entity spottedEnemy )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( spottedEnemy ) )
+ return
+
+ if ( spottedEnemy.GetTeam() == grunt.GetTeam() )
+ return
+
+ string alias = ""
+ float distToSpottedEnemy = Distance( grunt.GetOrigin(), spottedEnemy.GetOrigin() )
+
+ // TODO move to data files
+ if ( IsGrunt( spottedEnemy ) && TimerCheck( "chatter_enemy_grunt_spotted" ) && distToSpottedEnemy <= CHATTER_ENEMY_GRUNT_SPOTTED_DIST )
+ {
+ alias = "gruntchatter_enemy_grunt_spotted"
+ }
+ else if ( spottedEnemy.IsTitan() && TimerCheck( "chatter_enemy_titan_spotted" ) && distToSpottedEnemy <= CHATTER_ENEMY_TITAN_SPOTTED_DIST )
+ {
+ alias = "gruntchatter_enemy_titan_spotted"
+ if ( distToSpottedEnemy <= CHATTER_ENEMY_TITAN_SPOTTED_DIST_CLOSE )
+ alias = "gruntchatter_enemy_titan_spotted_close"
+ }
+ else if ( IsSpectre( spottedEnemy ) && TimerCheck( "chatter_enemy_spectre_spotted" ) && distToSpottedEnemy <= CHATTER_ENEMY_SPECTRE_SPOTTED_DIST )
+ {
+ alias = "gruntchatter_enemy_spectre_spotted"
+ if ( distToSpottedEnemy <= CHATTER_ENEMY_SPECTRE_SPOTTED_DIST_CLOSE )
+ alias = "gruntchatter_enemy_spectre_spotted_close"
+ }
+ else if ( IsTick( spottedEnemy ) && TimerCheck( "chatter_enemy_tick_spotted" ) && distToSpottedEnemy <= CHATTER_ENEMY_TICK_SPOTTED_DIST )
+ {
+ alias = "gruntchatter_enemy_tick_spotted"
+ }
+ else if ( IsPilotDecoy( spottedEnemy ) && TimerCheck( "chatter_enemy_pilot_decoy_spotted" ) && distToSpottedEnemy <= CHATTER_PILOT_DECOY_SPOTTED_DIST_MAX )
+ {
+ alias = "gruntchatter_enemy_pilot_decoy_spotted"
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias, grunt, spottedEnemy )
+}
+
+void function GruntChatter_TryEngagingNonPilotTarget( entity grunt, entity target )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ string alias = ""
+
+ if ( IsGrunt( target ) && TimerCheck( "chatter_engaging_grunt" ) )
+ {
+ alias = "gruntchatter_engaging_grunt"
+ }
+ else if ( IsSpectre( target ) && TimerCheck( "chatter_engaging_spectre" ) )
+ {
+ alias = "gruntchatter_engaging_spectre"
+ if ( IsValid( target.GetBossPlayer() ) )
+ alias = "gruntchatter_engaging_hacked_spectre"
+ }
+ else if ( IsProwler( target ) && TimerCheck( "chatter_engaging_prowler" ) )
+ {
+ alias = "gruntchatter_engaging_prowler"
+ }
+ else if ( IsStalker( target ) && TimerCheck( "chatter_engaging_stalker" ) )
+ {
+ alias = "gruntchatter_engaging_stalker"
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias, grunt, target )
+}
+
+void function GruntChatter_TryCloakedPilotSpotted( entity grunt, entity pilot )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( pilot ) )
+ return
+
+ if ( !IsCloaked( pilot ) )
+ return
+
+ // note: CanSee doesn't work when player is cloaked (as expected...)
+ if ( !GruntChatter_CanGruntTraceToLocation( grunt, pilot.EyePosition() ) )
+ return
+
+ if ( GruntChatter_IsTargetFacingAway( pilot, grunt, CHATTER_SEE_CLOAKED_PILOT_MIN_DOT_REAR ) )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_pilot_spotted_cloaked", grunt )
+}
+
+void function GruntChatter_TryPersonalShieldDamaged( entity shieldOwner )
+{
+ GruntChatter_AddEvent( "gruntchatter_personal_shield_damaged", shieldOwner )
+}
+
+void function GruntChatter_TryFriendlyEquipmentDeployed( entity deployer, string equipmentClassName )
+{
+ string alias = ""
+ string timerAlias = ""
+
+ // TODO move to data files
+ switch ( equipmentClassName )
+ {
+ case "npc_drone":
+ alias = "gruntchatter_friendly_drone_deployed"
+ timerAlias = "chatter_friendly_drone_deployed"
+ break
+
+ case "mp_weapon_frag_drone":
+ alias = "gruntchatter_friendly_tick_deployed"
+ timerAlias = "chatter_friendly_tick_deployed"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ if ( !TimerCheck( timerAlias ) )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( deployer.GetOrigin(), deployer.GetTeam(), CHATTER_FRIENDLY_EQUIPMENT_DEPLOYED_NEARBY_DIST )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( alias, closestGrunt )
+}
+
+void function GruntChatter_TryDisplacingFromDangerousArea( entity displacingGrunt )
+{
+ string dangerousAreaWeaponName = displacingGrunt.GetDangerousAreaWeapon()
+ GruntChatter_TryDangerousAreaWeapon( displacingGrunt, dangerousAreaWeaponName )
+}
+
+void function GruntChatter_TryDangerousAreaWeapon( entity grunt, string dangerousAreaWeaponName )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ string alias
+ string timerAlias
+
+ // TODO move to data files
+ switch ( dangerousAreaWeaponName )
+ {
+ case "mp_weapon_frag_grenade": //Since GruntChatter_TryDangerousAreaWeapon() is called from both CodeDialogue_DangerousAreaDisplace() and GruntChatter_OnGruntDamaged() this has bugs; a grunt who was not in the dangerous area created but took damage from the frag grenade will say VO like "Incoming Frag!! Take cover!". Not worth fixing this late in.
+ alias = "gruntchatter_dangerous_area_frag"
+ timerAlias = "chatter_dangerous_area_frag"
+ break
+
+ case "mp_weapon_grenade_emp": //This is triggered from GruntChatter_OnGruntDamaged(), since arc grenades don't create a dangerousarea
+ alias = "gruntchatter_dangerous_area_arc_grenade"
+ timerAlias = "chatter_dangerous_area_arc_grenade"
+ break
+
+ case "mp_weapon_thermite_grenade":
+ alias = "gruntchatter_dangerous_area_thermite"
+ timerAlias = "chatter_dangerous_area_thermite"
+ break
+
+ case "mp_weapon_grenade_gravity":
+ alias = "gruntchatter_dangerous_area_grav_grenade"
+ timerAlias = "chatter_dangerous_area_grav_grenade"
+ break
+
+ case "mp_weapon_grenade_electric_smoke":
+ alias = "gruntchatter_dangerous_area_esmoke"
+ timerAlias = "chatter_dangerous_area_esmoke"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ if ( !TimerCheck ( timerAlias ) )
+ return
+
+ // all grunts in the area will try to call it out, in case this guy dies
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( grunt.GetOrigin(), grunt.GetTeam(), CHATTER_DANGEROUS_AREA_NEARBY_RANGE )
+ foreach ( nearbyGrunt in nearbyGrunts )
+ GruntChatter_AddEvent( alias, nearbyGrunt )
+}
+
+void function GruntChatter_TryEnemyTimeShifted( entity timeShiftedEnemy )
+{
+ if ( !IsAlive( timeShiftedEnemy ) )
+ return
+
+ if ( !TimerCheck( "chatter_enemy_time_shifted" ) )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestEnemyHumanGrunt_LOS( timeShiftedEnemy.GetOrigin(), timeShiftedEnemy.GetTeam(), CHATTER_ENEMY_TIME_SHIFT_NEARBY_DIST )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_enemy_time_shifted", closestGrunt )
+}
+
+void function GruntChatter_OnGruntDamaged( entity grunt, var damageInfo )
+{
+ if ( !IsValid( grunt ) )
+ return
+
+ string damageWeaponName
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ table dmgSources = expect table( getconsttable().eDamageSourceId )
+ foreach ( name, id in dmgSources )
+ {
+ if ( id == damageSourceID )
+ {
+ damageWeaponName = expect string( name )
+ break
+ }
+ }
+
+ if ( damageWeaponName != "" )
+ GruntChatter_TryDangerousAreaWeapon( grunt, damageWeaponName )
+}
+
+void function GruntChatter_OnPlayerOrNPCKilled( entity deadGuy, entity attacker, var damageInfo )
+{
+ if ( !IsValid( deadGuy ) )
+ return
+
+ if ( deadGuy.GetTeam() == TEAM_IMC )
+ {
+ GruntChatter_TryEnemyPlayerPilot_Multikill( deadGuy, damageInfo )
+ GruntChatter_TryEnemyPlayerPilot_MobilityKill( deadGuy, damageInfo )
+ GruntChatter_TryFriendlyDown( deadGuy )
+ GruntChatter_TrySquadDepleted( deadGuy )
+ }
+ else
+ {
+ GruntChatter_TryEnemyDown( deadGuy )
+ }
+}
+
+void function GruntChatter_OnPilotDecoyKilled( entity decoy, var damageInfo )
+{
+ GruntChatter_TryEnemyDown( decoy )
+}
+
+void function GruntChatter_TryEnemyPlayerPilot_Multikill( entity deadGuy, var damageInfo )
+{
+ if ( !TimerCheck( "chatter_enemy_pilot_multikill" ) )
+ return
+
+ // don't worry about larger targets
+ if ( !IsHumanSized( deadGuy ) )
+ return
+
+ int customDamageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ // explosive kills don't count for pilot multikills
+ if ( customDamageType & DF_EXPLOSION )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsPilot( attacker ) )
+ return
+
+ // -- multikills --
+ if ( !GruntChatter_IsKillChainStillActive( attacker ) )
+ GruntChatter_ResetPilotKillChain( attacker )
+
+ GruntChatter_UpdatePilotKillChain( attacker )
+
+ if ( GruntChatter_GetPilotKillChain( attacker ) < CHATTER_PILOT_MULTIKILL_MIN_KILLS )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( deadGuy.GetOrigin(), deadGuy.GetTeam(), CHATTER_FRIENDLY_GRUNT_DOWN_DIST_MAX )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_enemy_pilot_multikill", closestGrunt, attacker )
+}
+
+void function GruntChatter_TryEnemyPlayerPilot_MobilityKill( entity deadGuy, var damageInfo )
+{
+ if ( !TimerCheck( "chatter_enemy_pilot_mobility_kill" ) )
+ return
+
+ // don't worry about larger targets
+ if ( !IsHumanSized( deadGuy ) )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsPilot( attacker ) )
+ return
+
+ if ( attacker.IsOnGround() )
+ return
+
+ float targetSpeed = Length( attacker.GetVelocity() )
+ if ( !attacker.IsWallRunning() && targetSpeed < CHATTER_MISS_FAST_TARGET_MIN_SPEED )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( deadGuy.GetOrigin(), deadGuy.GetTeam(), CHATTER_FRIENDLY_GRUNT_DOWN_DIST_MAX )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_enemy_pilot_mobility_kill", closestGrunt, attacker )
+}
+
+void function GruntChatter_TryFriendlyDown( entity deadGuy )
+{
+ string alias = ""
+ float searchRange = -1.0
+
+ if ( IsGrunt( deadGuy ) && TimerCheck( "chatter_friendly_grunt_down" ) )
+ {
+ alias = "gruntchatter_friendly_grunt_down"
+ if ( svGlobalSP.gruntCombatState == eGruntCombatState.IDLE )
+ alias = "gruntchatter_friendly_grunt_down_notarget"
+
+ searchRange = CHATTER_FRIENDLY_GRUNT_DOWN_DIST_MAX
+ }
+ else if ( deadGuy.IsTitan() && TimerCheck( "chatter_friendly_titan_down" ) )
+ {
+ alias = "gruntchatter_friendly_titan_down"
+ searchRange = CHATTER_FRIENDLY_TITAN_DOWN_DIST_MAX
+ }
+
+ if ( alias == "" )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( deadGuy.GetOrigin(), deadGuy.GetTeam(), searchRange )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( alias, closestGrunt )
+}
+
+void function GruntChatter_TrySquadDepleted( entity deadGuy )
+{
+ if ( !TimerCheck( "chatter_squad_depleted" ) )
+ return
+
+ if ( !IsGrunt( deadGuy ) )
+ return
+
+ string deadGuySquadName = GetSquadName( deadGuy )
+ if ( deadGuySquadName == "" )
+ return
+
+ array<entity> squad = GetNPCArrayBySquad( deadGuySquadName )
+ entity lastSquadMember
+ if ( squad.len() == 1 )
+ lastSquadMember = squad[0]
+
+ if ( !GruntChatter_CanGruntChatterNow( lastSquadMember ) )
+ return
+
+ // if state is idle, don't freak out about being alone
+ if ( lastSquadMember.GetNPCState() == "idle" )
+ return
+
+ // if another grunt from another squad is nearby, don't chatter about being alone
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( lastSquadMember.GetOrigin(), lastSquadMember.GetTeam(), CHATTER_SQUAD_DEPLETED_FRIENDLY_NEARBY_DIST )
+ nearbyGrunts.fastremovebyvalue( lastSquadMember )
+ if ( nearbyGrunts.len() )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_squad_depleted", lastSquadMember )
+}
+
+void function GruntChatter_TryEnemyDown( entity deadGuy )
+{
+ string alias = ""
+ float searchRange = -1.0
+
+ if ( IsPilot( deadGuy ) && TimerCheck( "chatter_enemy_pilot_down" ) )
+ {
+ alias = "gruntchatter_enemy_pilot_down"
+ searchRange = CHATTER_ENEMY_PILOT_DOWN_DIST_MAX
+ }
+ else if ( IsGrunt( deadGuy ) && TimerCheck( "chatter_enemy_grunt_down" ) )
+ {
+ alias = "gruntchatter_enemy_grunt_down"
+ searchRange = CHATTER_ENEMY_GRUNT_DOWN_DIST_MAX
+ }
+ else if ( deadGuy.IsTitan() && TimerCheck( "chatter_enemy_titan_down" ) )
+ {
+ alias = "gruntchatter_enemy_titan_down"
+ searchRange = CHATTER_ENEMY_TITAN_DOWN_DIST_MAX
+ }
+ else if ( IsSpectre( deadGuy ) && TimerCheck( "chatter_enemy_spectre_down" ) )
+ {
+ alias = "gruntchatter_enemy_spectre_down"
+ searchRange = CHATTER_ENEMY_SPECTRE_DOWN_DIST_MAX
+ }
+ else if ( IsPilotDecoy( deadGuy ) && TimerCheck( "chatter_enemy_pilot_decoy_revealed" ) )
+ {
+ alias = "gruntchatter_enemy_pilot_decoy_revealed"
+ searchRange = CHATTER_PILOT_DECOY_SPOTTED_DIST_MAX
+ }
+
+ if ( alias == "" )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestEnemyHumanGrunt_LOS( deadGuy.GetOrigin(), deadGuy.GetTeam(), searchRange )
+ if ( !closestGrunt )
+ return
+
+ // HACK- squad conversations don't play to dead players
+ if ( alias == "gruntchatter_enemy_pilot_down" )
+ {
+ HACK_GruntChatter_TryEnemyPilotDown( deadGuy, closestGrunt )
+ return
+ }
+
+ GruntChatter_AddEvent( alias, closestGrunt )
+}
+
+void function HACK_GruntChatter_TryEnemyPilotDown( entity deadGuy, entity closestGrunt )
+{
+ if ( !deadGuy.IsPlayer() )
+ return
+
+ if ( deadGuy.GetForcedDialogueOnly() )
+ return
+
+ TimerReset( "chatter_enemy_pilot_down" )
+
+ string rawAlias = "diag_imc_grunt1_bc_killenemypilot_01"
+ if ( CoinFlip() )
+ rawAlias = "diag_imc_grunt1_bc_killenemypilot_02"
+
+ EmitSoundOnEntity( closestGrunt, rawAlias )
+}
+
+void function GruntChatter_TryThrowingGrenade( entity grunt )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ entity enemy = grunt.GetEnemy()
+ if ( !IsAlive( enemy ) )
+ return
+
+ if ( !TimerCheck( "chatter_throwing_grenade" ) )
+ return
+
+ string alias = ""
+ // TODO move to data files
+ switch ( grunt.kv.grenadeWeaponName )
+ {
+ case "mp_weapon_frag_grenade":
+ alias = "gruntchatter_throwing_grenade_frag"
+ break
+
+ case "mp_weapon_grenade_electric_smoke":
+ alias = "gruntchatter_throwing_grenade_electric_smoke"
+ break
+
+ case "mp_weapon_thermite_grenade":
+ alias = "gruntchatter_throwing_grenade_thermite"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias, grunt )
+}
+
+// TODO move to data files
+void function GruntChatter_TryFriendlyPassingNearby( entity grunt )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ // these lines are written as if the grunts are in combat
+ if ( grunt.GetNPCState() != "combat" )
+ return
+
+ if ( TimerCheck( "chatter_nearby_friendly_titan" ) )
+ {
+ array<entity> nearbyTitans = GetNPCArrayEx( "npc_titan", grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), CHATTER_NEARBY_TITAN_DIST )
+ entity nearbyTitan
+ foreach ( titan in nearbyTitans )
+ {
+ if ( !IsAlive( titan ) )
+ continue
+
+ if ( GetDoomedState( titan ) )
+ continue
+
+ if ( GruntChatter_EventTargetAlreadyUsed( titan ) )
+ continue
+
+ nearbyTitan = titan
+ break
+ }
+
+ if ( nearbyTitan )
+ GruntChatter_AddEvent( "gruntchatter_nearby_friendly_titan", grunt, nearbyTitan )
+ }
+
+ if ( TimerCheck( "chatter_nearby_friendly_reaper" ) )
+ {
+ array<entity> nearbyReapers = GetNPCArrayEx( "npc_super_spectre", grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), CHATTER_NEARBY_REAPER_DIST )
+ foreach ( reaper in nearbyReapers )
+ {
+ if ( !IsAlive( reaper ) )
+ continue
+
+ if ( GetDoomedState( reaper ) )
+ continue
+
+ if ( GruntChatter_EventTargetAlreadyUsed( reaper ) )
+ continue
+
+ GruntChatter_AddEvent( "gruntchatter_nearby_friendly_reaper", grunt, reaper )
+ break
+ }
+ }
+
+ if ( TimerCheck( "chatter_nearby_friendly_spectre" ) )
+ {
+ array<entity> nearbySpectres = GetNPCArrayEx( "npc_spectre", grunt.GetTeam(), TEAM_ANY, grunt.GetOrigin(), CHATTER_NEARBY_SPECTRE_DIST )
+ if ( nearbySpectres.len() )
+ {
+ entity closestSpectre = GetClosest( nearbySpectres, grunt.GetOrigin() )
+ GruntChatter_AddEvent( "gruntchatter_nearby_friendly_spectre", grunt, closestSpectre )
+ }
+ }
+}
+
+void function GruntChatter_TryIncomingSpawn( entity inboundEnt, vector arrivalLocation )
+{
+ if ( !IsValid( inboundEnt ) )
+ return
+
+ string alias
+ string timer
+ float nearbyRange
+ entity closestGrunt
+
+ // TODO move to data files
+ if ( inboundEnt.GetTeam() == TEAM_IMC )
+ {
+ switch ( inboundEnt.GetClassName() )
+ {
+ case "npc_titan":
+ alias = "gruntchatter_incoming_friendly_titanfall"
+ timer = "chatter_incoming_friendly_titanfall"
+ nearbyRange = CHATTER_NEARBY_TITAN_DIST
+ break
+
+ case "npc_super_spectre":
+ alias = "gruntchatter_incoming_friendly_reaperfall"
+ timer = "chatter_incoming_friendly_reaperfall"
+ nearbyRange = CHATTER_NEARBY_REAPER_DIST
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( arrivalLocation, inboundEnt.GetTeam(), nearbyRange )
+ if ( !closestGrunt )
+ return
+ }
+ else if ( inboundEnt.GetTeam() == TEAM_MILITIA )
+ {
+ switch ( inboundEnt.GetClassName() )
+ {
+ case "npc_titan":
+ alias = "gruntchatter_incoming_enemy_titanfall"
+ timer = "chatter_incoming_enemy_titanfall"
+ nearbyRange = CHATTER_NEARBY_TITAN_DIST
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ closestGrunt = GruntChatter_FindClosestEnemyHumanGrunt_LOS( arrivalLocation, inboundEnt.GetTeam(), nearbyRange )
+ if ( !closestGrunt )
+ return
+ }
+
+ // NOTE- can't send the target for these events because the distance check to where the titanfall starts will fail
+ GruntChatter_AddEvent( alias, closestGrunt )
+}
+
+void function GruntChatter_TrySuppressingPilotTarget( entity grunt, entity target )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ // this is mostly useful for players
+ if ( !target.IsPlayer() )
+ return
+
+ if ( !IsPilot( target ) )
+ return
+
+ if ( !TimerCheck( "chatter_suppressingLKP_start" ) )
+ return
+
+ string STR_lastSuppressionTime = expect string( grunt.kv.lastSuppressionTime ) // hacky
+ float lastSuppressionTime = STR_lastSuppressionTime.tofloat()
+ float validRecentWindow_suppression = Time() - CHATTER_SUPPRESSION_EXPIRE_TIME
+
+ if ( lastSuppressionTime < validRecentWindow_suppression )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_suppressingLKP_start", grunt )
+}
+
+void function GruntChatter_TryMissingFastTarget( entity grunt, entity target )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ if ( !IsPilot( target ) )
+ return
+
+ if ( !TimerCheck( "chatter_missing_fast_target" ) )
+ return
+
+ float targetSpeed = Length( target.GetVelocity() )
+ if ( targetSpeed < CHATTER_MISS_FAST_TARGET_MIN_SPEED )
+ return
+
+ string STR_lastMissFastPlayerTime = expect string( grunt.kv.lastMissFastPlayerTime ) // hacky
+ float lastMissFastPlayerTime = STR_lastMissFastPlayerTime.tofloat()
+ float validRecentWindow_missFastTarget = Time() - CHATTER_MISS_FAST_TARGET_EXPIRE_TIME
+
+ if ( lastMissFastPlayerTime < validRecentWindow_missFastTarget )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_missing_fast_target", grunt )
+}
+
+void function GruntChatter_TryPilotLowHealth( entity grunt, entity target )
+{
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return
+
+ if ( !IsAlive( target ) )
+ return
+
+ if ( !IsPilot( target ) )
+ return
+
+ if ( !TimerCheck( "chatter_pilot_low_health" ) )
+ return
+
+ if ( target.GetHealth().tofloat() / target.GetMaxHealth().tofloat() > CHATTER_PILOT_LOW_HEALTH_FRAC )
+ return
+
+ if ( Distance( grunt.GetOrigin(), target.GetOrigin() ) > CHATTER_PILOT_LOW_HEALTH_RANGE )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_pilot_low_health", grunt )
+}
+
+void function GruntChatter_TryPlayerPilotReloading( entity player )
+{
+ if ( !IsAlive( player ) || !IsPilot( player ) )
+ return
+
+ if ( !TimerCheck( "chatter_target_reloading" ) )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestEnemyHumanGrunt_LOS( player.GetOrigin(), player.GetTeam(), CHATTER_PLAYER_RELOADING_RANGE )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_target_reloading", closestGrunt, player )
+}
+
+
+// ==== turret event handling ====
+void function GruntChatter_TurretSignalWait( entity turret )
+{
+ turret.EndSignal( "OnDeath" )
+ turret.EndSignal( "OnDestroy" )
+
+ while ( 1 )
+ {
+ table result = WaitSignal( turret, "OnFoundEnemy", "OnSeeEnemy" )
+
+ string signal = expect string( result.signal )
+
+ switch( signal )
+ {
+ case "OnFoundEnemy":
+ entity enemy = expect entity( result.value )
+ GruntChatter_TryFriendlyTurretFoundTarget( turret, enemy )
+ break
+
+ case "OnSeeEnemy":
+ entity enemy = expect entity( result.activator )
+ GruntChatter_TryFriendlyTurretFoundTarget( turret, enemy )
+ break
+
+ }
+ }
+}
+
+void function GruntChatter_TryFriendlyTurretFoundTarget( entity turret, entity enemy )
+{
+ if ( !IsAlive( turret ) || !IsAlive( enemy ) )
+ return
+
+ if ( !TimerCheck( "chatter_friendly_turret_found_target") )
+ return
+
+ entity closestGrunt = GruntChatter_FindClosestFriendlyHumanGrunt_LOS( turret.GetOrigin(), turret.GetTeam(), CHATTER_FRIENDLY_EQUIPMENT_DEPLOYED_NEARBY_DIST )
+ if ( !closestGrunt )
+ return
+
+ GruntChatter_AddEvent( "gruntchatter_friendly_turret_found_target", closestGrunt, enemy )
+}
+
+
+// ==== pilot kill chains ====
+// NOTE: don't technically require a pilot, but makes it easier to port to an MP environment
+void function GruntChatter_UpdatePilotKillChain( entity pilot )
+{
+ file.pilotKillChainCounter++
+ file.lastPilotKillTime = Time()
+}
+
+int function GruntChatter_GetPilotKillChain( entity pilot )
+{
+ return file.pilotKillChainCounter
+}
+
+bool function GruntChatter_IsKillChainStillActive( entity pilot )
+{
+ if ( file.lastPilotKillTime == -1 )
+ return true
+
+ return (Time() - file.lastPilotKillTime) < CHATTER_ENEMY_PILOT_MULTIKILL_EXPIRETIME
+}
+
+void function GruntChatter_ResetPilotKillChain( entity pilot )
+{
+ file.pilotKillChainCounter = 0
+}
+
+
+// ==== chatter util ====
+// won't return mechanicals like Specialists
+array<entity> function GetNearbyFriendlyHumanGrunts( vector searchOrigin, int friendlyTeam, float ornull searchRange = null )
+{
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( searchOrigin, friendlyTeam, searchRange )
+ array<entity> humanGrunts = []
+ foreach ( grunt in nearbyGrunts )
+ {
+ if ( grunt.IsMechanical() )
+ continue
+
+ humanGrunts.append( grunt )
+ }
+
+ return humanGrunts
+}
+
+// won't return mechanicals like Specialists
+array<entity> function GetNearbyEnemyHumanGrunts( vector searchOrigin, int enemyTeam, float ornull searchRange = null )
+{
+ array<entity> nearbyGrunts = GetNearbyEnemyGrunts( searchOrigin, enemyTeam, searchRange )
+ array<entity> humanGrunts = []
+ foreach ( grunt in nearbyGrunts )
+ {
+ if ( grunt.IsMechanical() )
+ continue
+
+ humanGrunts.append( grunt )
+ }
+
+ return humanGrunts
+}
+
+bool function GruntChatter_CanGruntChatterNow( entity grunt )
+{
+ if ( !IsAlive( grunt ) )
+ return false
+
+ if ( !GruntChatter_IsGruntTypeEligibleForChatter( grunt ) )
+ return false
+
+ if ( grunt.ContextAction_IsMeleeExecution() )
+ return false
+
+ // we only care about this because the grunt conversation system wants it
+ if ( GetSquadName( grunt ) == "" )
+ return false
+
+ return true
+}
+
+bool function GruntChatter_IsGruntTypeEligibleForChatter( entity grunt )
+{
+ if ( !IsGrunt( grunt ) )
+ return false
+
+ // mechanical grunts don't chatter
+ if ( grunt.IsMechanical() )
+ return false
+
+ if ( grunt.GetTeam() != TEAM_IMC )
+ return false
+
+ return true
+}
+
+bool function GruntChatter_CanGruntChatterToPlayer( entity grunt, entity player )
+{
+ if ( DistanceSqr( grunt.GetOrigin(), player.GetOrigin() ) > MAX_VOICE_DIST_SQRD )
+ return false
+
+ return true
+}
+
+bool function GruntChatter_CanChatterEventUseEnemyTarget( ChatterEvent chatterEvent )
+{
+ entity grunt = chatterEvent.npc
+ entity target = chatterEvent.target
+ bool trackEventTarget = chatterEvent.category.trackEventTarget
+
+ if ( !chatterEvent.hasTarget )
+ return false
+
+ if ( !IsAlive( target ) )
+ return false
+
+ if ( trackEventTarget && GruntChatter_EventTargetAlreadyUsed( target ) )
+ return false
+
+ float distToEnemySqr = DistanceSqr( grunt.GetOrigin(), target.GetOrigin() )
+ if ( distToEnemySqr > MAX_VOICE_DIST_SQRD )
+ return false
+
+ return true
+}
+
+bool function CanNearbyGruntTeammatesSeeEnemy( entity grunt, entity enemy, float nearbyRange )
+{
+ if ( !IsAlive( enemy ) )
+ return false
+
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( enemy.GetOrigin(), grunt.GetTeam(), nearbyRange )
+
+ foreach ( grunt in nearbyGrunts )
+ {
+ if ( grunt.CanSee( enemy ) )
+ return true
+ }
+
+ return false
+}
+
+bool function GruntChatter_IsFriendlyGruntCloseToLocation( int team, vector location, float nearbyRange )
+{
+ array<entity> nearbyGrunts = GetNearbyFriendlyGrunts( location, team, nearbyRange )
+
+ if ( nearbyGrunts.len() )
+ return true
+
+ return false
+}
+
+bool function GruntChatter_IsTargetFacingAway( entity grunt, entity target, float minDotRear )
+{
+ if ( !IsAlive( grunt ) || !IsAlive( target ) )
+ return false
+
+ vector viewAng = target.GetAngles() // overall body angles better for this than viewvec
+ vector viewVec = AnglesToForward( viewAng )
+ vector vecRear = viewVec * -1
+ vector angRear = VectorToAngles( vecRear )
+
+ vector vecToTarget = Normalize( grunt.EyePosition() - target.EyePosition() )
+ float dot2Grunt_rear = DotProduct( vecToTarget, vecRear )
+
+ //printt( "REAR dot to enemy:", dot2Grunt_rear )
+
+ return dot2Grunt_rear >= minDotRear
+}
+
+bool function GruntChatter_IsEnemyAbove( entity grunt, entity enemy )
+{
+ // Pilots jumping over guys gives false positives
+ if ( IsPilot( enemy ) && !enemy.IsOnGround() )
+ return false
+
+ vector gOrg = grunt.GetOrigin()
+ vector eOrg = enemy.GetOrigin()
+
+ vector cylinderBottom = gOrg + < 0, 0, CHATTER_PILOT_SPOTTED_ABOVE_DIST_MIN >
+ vector cylinderTop = gOrg + < 0, 0, CHATTER_PILOT_SPOTTED_ABOVE_DIST_MAX >
+
+ bool isAbove = PointInCylinder( cylinderBottom, cylinderTop, CHATTER_PILOT_SPOTTED_ABOVE_RADIUS, eOrg )
+ return isAbove
+}
+
+bool function GruntChatter_IsEnemyBelow( entity grunt, entity enemy )
+{
+ vector gOrg = grunt.GetOrigin()
+ vector eOrg = enemy.GetOrigin()
+
+ vector cylinderBottom = gOrg - < 0, 0, CHATTER_PILOT_SPOTTED_BELOW_DIST_MAX >
+ vector cylinderTop = gOrg - < 0, 0, CHATTER_PILOT_SPOTTED_BELOW_DIST_MIN >
+
+ bool isBelow = PointInCylinder( cylinderBottom, cylinderTop, CHATTER_PILOT_SPOTTED_BELOW_RADIUS, eOrg )
+ return isBelow
+}
+
+void function GruntChatter_TryGruntFlankedByPlayer( entity grunt, int aiSurprisedReactionType )
+{
+ if ( !GruntChatter_CanGruntDoFlankingCallout( grunt ) )
+ return
+
+ entity surprisingEnemy = grunt.GetEnemy()
+ if ( !IsPilot( surprisingEnemy ) || !surprisingEnemy.IsPlayer() )
+ return
+
+ string alias
+ switch ( aiSurprisedReactionType )
+ {
+ case RSR_REAR_FLANK:
+ //printt( "REAR FLANK!")
+ alias = "gruntchatter_pilot_spotted_flank_rear"
+ break
+
+ case RSR_SIDE_FLANK:
+ //printt( " SIDE FLANK!" )
+ alias = "gruntchatter_pilot_spotted_flank_side"
+ break
+ }
+
+ if ( alias == "" )
+ return
+
+ GruntChatter_AddEvent( alias, grunt, surprisingEnemy )
+}
+
+bool function GruntChatter_CanGruntDoFlankingCallout( entity grunt )
+{
+ if ( !TimerCheck( "chatter_pilot_flanking" ) )
+ return false
+
+ if ( !GruntChatter_CanGruntChatterNow( grunt ) )
+ return false
+
+ return true
+}
+
+entity function GruntChatter_FindClosestEnemyHumanGrunt_LOS( vector searchOrigin, int enemyTeam, float searchDist )
+{
+ array<entity> humanGrunts = GetNearbyEnemyHumanGrunts( searchOrigin, enemyTeam, searchDist )
+ return GruntChatter_GetClosestGrunt_LOS( humanGrunts, searchOrigin )
+}
+
+entity function GruntChatter_FindClosestFriendlyHumanGrunt_LOS( vector searchOrigin, int friendlyTeam, float searchDist )
+{
+ array<entity> humanGrunts = GetNearbyFriendlyHumanGrunts( searchOrigin, friendlyTeam, searchDist )
+ return GruntChatter_GetClosestGrunt_LOS( humanGrunts, searchOrigin )
+}
+
+entity function GruntChatter_GetClosestGrunt_LOS( array<entity> nearbyGrunts, vector searchOrigin )
+{
+ entity closestGrunt = null
+ float closestDist = 10000
+
+ foreach ( grunt in nearbyGrunts )
+ {
+ vector gruntOrigin = grunt.GetOrigin()
+
+ // CanSee doesn't return true if the target is dead
+ if ( !GruntChatter_CanGruntTraceToLocation( grunt, searchOrigin ) )
+ continue
+
+ if ( !closestGrunt )
+ {
+ closestGrunt = grunt
+ continue
+ }
+
+ float distFromSearchOrigin = Distance( grunt.GetOrigin(), searchOrigin )
+
+ if ( closestDist > distFromSearchOrigin )
+ continue
+
+ closestGrunt = grunt
+ closestDist = distFromSearchOrigin
+ }
+
+ return closestGrunt
+}
+
+bool function GruntChatter_CanGruntTraceToLocation( entity grunt, vector traceEnd )
+{
+ float traceFrac = TraceLineSimple( grunt.GetOrigin(), traceEnd, grunt )
+ return traceFrac > CHATTER_NEARBY_GRUNT_TRACEFRAC_MIN
+}
+
+string function GetSquadName( entity grunt )
+{
+ string squadName = expect string( grunt.kv.squadname )
+ return squadName
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_squad_spawn.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_squad_spawn.gnut
new file mode 100644
index 000000000..9dbdd699d
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_squad_spawn.gnut
@@ -0,0 +1,167 @@
+
+
+
+global function GetNPCBaseClassFromSpawnFunc
+
+global function CreateZipLineSquadDropTable
+
+string function GetNPCBaseClassFromSpawnFunc( entity functionref( int, vector, vector ) spawnFunc )
+{
+ // temp spawn a guy to get his baseclass.
+ entity npc = spawnFunc( TEAM_IMC, Vector(0,0,0), Vector(0,0,0) )
+ string baseClass = npc.GetClassName()
+ npc.Destroy()
+ return baseClass
+}
+
+
+
+void function DropOffAISide_NPCThink( entity npc, int index, entity dropship, string attach )
+{
+ npc.EndSignal( "OnDeath" )
+
+ //init
+ npc.SetParent( dropship, attach )
+ npc.SetEfficientMode( true )
+
+ //deploy
+ array<string> deployAnims = DropOffAISide_GetDeployAnims()
+ array<float> seekTimes = DropOffAISide_GetSeekTimes()
+
+ thread PlayAnimTeleport( npc, deployAnims[ index ], dropship, attach )
+ npc.Anim_SetInitialTime( seekTimes[ index ] )
+ WaittillAnimDone( npc )
+
+ npc.SetEfficientMode( false )
+
+ //disperse
+ array<string> disperseAnims = DropOffAISide_GetDisperseAnims()
+ vector origin = HackGetDeltaToRef( npc.GetOrigin(), npc.GetAngles(), npc, disperseAnims[ index ] ) + Vector( 0,0,2 )
+ waitthread PlayAnimGravity( npc, disperseAnims[ index ], origin, npc.GetAngles() )
+}
+
+void function DropOffAISide_WarpOutShip( entity dropship, vector origin, vector angles )
+{
+ wait 1.5
+ dropship.EndSignal( "OnDeath" )
+
+ string anim = "cd_dropship_rescue_side_end"
+ thread PlayAnim( dropship, anim, origin, angles )
+
+ //blend
+ wait dropship.GetSequenceDuration( anim ) - 0.2
+
+ dropship.Hide()
+ thread WarpoutEffect( dropship )
+}
+
+float function GetInstantSpawnRadius( entity npc )
+{
+ float radius = 64
+
+ if ( npc )
+ {
+ switch ( npc.GetClassName() )
+ {
+ case "npc_gunship":
+ case "npc_dropship":
+ radius = 512
+ break
+
+ case "npc_titan":
+ radius = 256
+ break
+
+ case "npc_super_spectre":
+ case "npc_prowler":
+ radius = 128
+ break
+
+ default:
+ radius = 64
+ break
+ }
+ }
+
+ return radius
+}
+
+
+
+
+/************************************************************************************************\
+
+## ## #### ###### ###### ######## ####### ####### ## ######
+### ### ## ## ## ## ## ## ## ## ## ## ## ## ##
+#### #### ## ## ## ## ## ## ## ## ## ##
+## ### ## ## ###### ## ## ## ## ## ## ## ######
+## ## ## ## ## ## ## ## ## ## ## ##
+## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
+## ## #### ###### ###### ## ####### ####### ######## ######
+
+\************************************************************************************************/
+
+
+
+
+array<string> function DropOffAISide_GetIdleAnims()
+{
+ array<string> anims = [
+ "pt_ds_side_intro_gen_idle_A", //standing right
+ "pt_ds_side_intro_gen_idle_B", //standing left
+ "pt_ds_side_intro_gen_idle_C", //sitting right
+ "pt_ds_side_intro_gen_idle_D" ] //sitting left
+
+ return anims
+}
+
+array<string> function DropOffAISide_GetDeployAnims()
+{
+ array<string> anims = [
+ "pt_generic_side_jumpLand_A", //standing right
+ "pt_generic_side_jumpLand_B", //standing left
+ "pt_generic_side_jumpLand_C", //sitting right
+ "pt_generic_side_jumpLand_D" ] //sitting left
+
+ return anims
+}
+
+array<string> function DropOffAISide_GetDisperseAnims()
+{
+ array<string> anims = [
+ "React_signal_thatway", //standing right
+ "React_spot_radio2", //standing left
+ "stand_2_run_45R", //sitting right
+ "stand_2_run_45L" ] //sitting left
+
+ return anims
+}
+
+array<float> function DropOffAISide_GetSeekTimes()
+{
+ array<float> anims = [
+ 9.75, //standing right
+ 10.0, //standing left
+ 10.5, //sitting right
+ 11.25 ] //sitting left
+
+ return anims
+}
+
+
+CallinData function CreateZipLineSquadDropTable( int team, int count, vector origin, vector angles, string squadName = "" )
+{
+ if ( squadName == "" )
+ squadName = MakeSquadName( team, UniqueString( "ZiplineTable" ) )
+
+ CallinData drop
+ drop.origin = origin
+ drop.yaw = angles.y
+ drop.dist = 768
+ drop.team = team
+ drop.squadname = squadName
+ SetDropTableSpawnFuncs( drop, CreateSoldier, count )
+ SetCallinStyle( drop, eDropStyle.ZIPLINE_NPC )
+
+ return drop
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_titan_npc_behavior.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_titan_npc_behavior.gnut
new file mode 100644
index 000000000..347cb6441
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_titan_npc_behavior.gnut
@@ -0,0 +1,404 @@
+untyped
+
+global function TitanNpcBehavior_Init
+
+global function TitanNPC_Think
+global function TitanNPC_WaitForBubbleShield_StartAutoTitanBehavior
+global function TitanStandUp
+global function TitanKneel
+global function GetBubbleShieldDuration
+global function ShowMainTitanWeapons
+
+global function ChangedStance
+
+global function TitanWaitsToChangeStance
+global function ShouldBecomeAutoTitan
+
+function TitanNpcBehavior_Init()
+{
+ FlagInit( "DisableTitanKneelingEmbark" )
+
+ RegisterSignal( "TitanStopsThinking" )
+ RegisterSignal( "RodeoRiderChanged" )
+
+ if ( IsMultiplayer() )
+ {
+ AddCallback_OnTitanBecomesPilot( OnClassChangeBecomePilot )
+ AddCallback_OnPilotBecomesTitan( OnClassChangeBecomeTitan )
+ }
+}
+
+void function OnClassChangeBecomePilot( entity player, entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( !SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ {
+ entity ordnanceWeapon = titan.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ if ( IsValid( ordnanceWeapon ) )
+ ordnanceWeapon.AllowUse( false )
+
+ entity centerWeapon = titan.GetOffhandWeapon( OFFHAND_TITAN_CENTER )
+ if ( IsValid( centerWeapon ) )
+ centerWeapon.AllowUse( false )
+ }
+}
+
+void function OnClassChangeBecomeTitan( entity player, entity titan )
+{
+ entity soul = player.GetTitanSoul()
+
+ entity ordnanceWeapon = player.GetOffhandWeapon( OFFHAND_ORDNANCE )
+ if ( IsValid( ordnanceWeapon ) )
+ ordnanceWeapon.AllowUse( true )
+
+ entity centerWeapon = player.GetOffhandWeapon( OFFHAND_TITAN_CENTER )
+ if ( IsValid( centerWeapon ) )
+ centerWeapon.AllowUse( true )
+}
+
+float function GetBubbleShieldDuration( entity player )
+{
+ if ( PlayerHasPassive( player, ePassives.PAS_LONGER_BUBBLE ) )
+ return EMBARK_TIMEOUT + 10.0
+ else
+ return EMBARK_TIMEOUT
+
+ unreachable
+}
+
+void function TitanNPC_WaitForBubbleShield_StartAutoTitanBehavior( entity titan )
+{
+ Assert( IsAlive( titan ) )
+
+ titan.Signal( "TitanStopsThinking" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "TitanStopsThinking" )
+ titan.EndSignal( "ContextAction_SetBusy" )
+
+ entity bossPlayer = titan.GetBossPlayer()
+ if ( !bossPlayer )
+ return
+
+ OnThreadEnd(
+ function () : ( titan )
+ {
+ if ( IsAlive( titan ) )
+ {
+ titan.SetNoTarget( false )
+ thread TitanNPC_Think( titan )
+ }
+ }
+ )
+
+ titan.EndSignal( "ChangedTitanMode" )
+
+ float timeout
+ if ( SoulHasPassive( titan.GetTitanSoul(), ePassives.PAS_BUBBLESHIELD ) )
+ {
+ entity player = titan.GetBossPlayer()
+ timeout = GetBubbleShieldDuration( player )
+ }
+ else
+ {
+ timeout = 0
+ }
+
+ wait timeout
+}
+
+function TitanNPC_Think( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+
+ // JFS - Shouldn't have to check for presence of soul.
+ // The real fix for next game would be to make sure no other script can run between transferring away a titan's soul and destroying the titan.
+ // This particular bug occurred if TitanNPC_WaitForBubbleShield_StartAutoTitanBehavior() was called before soul transferred from npc to player,
+ // in which case the soul transfer killed the thread via Signal( "TitanStopsThinking" ), which causes the OnThreadEnd() to run TitanNPC_Think().
+ if ( !IsValid( soul ) )
+ return;
+
+ if ( soul.capturable || !ShouldBecomeAutoTitan( titan ) )
+ {
+ // capturable titan just kneels
+ if ( soul.GetStance() > STANCE_KNEELING )
+ thread TitanKneel( titan )
+ return
+ }
+
+ Assert( IsAlive( titan ) )
+
+ if ( !TitanCanStand( titan ) )// sets the var
+ {
+ // try to put the titan on the navmesh
+ vector ornull clampedPos = NavMesh_ClampPointForAIWithExtents( titan.GetOrigin(), titan, < 100, 100, 100 > )
+ if ( clampedPos != null )
+ {
+ expect vector( clampedPos )
+ titan.SetOrigin( clampedPos )
+ TitanCanStand( titan )
+ }
+ }
+
+ if ( !titan.GetBossPlayer() )
+ {
+ titan.Signal( "TitanStopsThinking" )
+ return
+ }
+
+ if ( "disableAutoTitanConversation" in titan.s ) //At this point the Titan has stood up and is ready to talk
+ delete titan.s.disableAutoTitanConversation
+
+ titan.EndSignal( "TitanStopsThinking" )
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "player_embarks_titan" )
+
+ // kneel in certain circumstances
+ for ( ;; )
+ {
+ if ( !ChangedStance( titan ) )
+ waitthread TitanWaitsToChangeStance( titan )
+ }
+}
+
+bool function ChangedStance( entity titan )
+{
+ if ( GetEmbarkDisembarkPlayer( titan ) )
+ return false
+
+ local soul = titan.GetTitanSoul()
+
+ // in a scripted sequence?
+ if ( IsValid( titan.GetParent() ) )
+ return false
+
+ if ( soul.GetStance() > STANCE_KNEELING )
+ {
+ if ( TitanShouldKneel( titan ) )
+ {
+ //waitthread PlayAnimGravity( titan, "at_MP_stand2knee_straight" )
+ waitthread KneelToShowRider( titan )
+ thread PlayAnim( titan, "at_MP_embark_idle_blended" )
+ SetStanceKneel( soul )
+ return true
+ }
+ }
+ else
+ {
+ if ( !TitanShouldKneel( titan ) && TitanCanStand( titan ) )
+ {
+ waitthread TitanStandUp( titan )
+ return true
+ }
+
+ if ( soul.GetStance() == STANCE_KNEEL )
+ {
+ thread PlayAnim( titan, "at_MP_embark_idle_blended" )
+ }
+ }
+
+ return false
+}
+
+function TitanShouldKneel( entity titan )
+{
+ local soul = titan.GetTitanSoul()
+
+ if ( soul.capturable )
+ return true
+
+ //if( HasEnemyRodeoRiders( titan ) )
+ // return true
+ if ( !TitanCanStand( titan ) )
+ return false
+
+ if ( !ShouldBecomeAutoTitan( titan ) )
+ return true
+
+ return false
+}
+
+function TitanWaitsToChangeStance( titan )
+{
+ local soul = titan.GetTitanSoul()
+ soul.EndSignal( "RodeoRiderChanged" )
+
+ titan.EndSignal( "OnAnimationInterrupted" )
+ titan.EndSignal( "OnAnimationDone" )
+
+ WaitForever()
+}
+
+function TitanStandUp( titan )
+{
+ local soul = titan.GetTitanSoul()
+ // stand up
+ titan.s.standQueued = false
+ ShowMainTitanWeapons( titan )
+ titan.Anim_Stop()
+ waitthread PlayAnimGravity( titan, "at_hotdrop_quickstand" )
+ Assert( soul == titan.GetTitanSoul() )
+ SetStanceStand( soul )
+}
+
+
+void function TitanKneel( entity titan )
+{
+ titan.EndSignal( "TitanStopsThinking" )
+ titan.EndSignal( "OnDeath" )
+ Assert( IsAlive( titan ) )
+ local soul = titan.GetTitanSoul()
+
+ waitthread KneelToShowRider( titan )
+
+ thread PlayAnim( titan, "at_MP_embark_idle_blended" )
+ SetStanceKneel( soul )
+}
+
+
+/*
+function TitanWaittillShouldStand( entity titan )
+{
+ //Don't wait if player is dead - titan should just stand up immediately
+ local player = titan.GetBossPlayer()
+ if ( !IsAlive( player ) )
+ return
+
+ player.EndSignal( "OnDeath" )
+
+ for ( ;; )
+ {
+ if ( TitanCanStand( titan ) )
+ break
+
+ wait 5
+ }
+ if ( titan.s.standQueued )
+ return
+
+ titan.WaitSignal( "titanStand" )
+}
+*/
+
+void function KneelToShowRider( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+ entity player = soul.GetBossPlayer()
+ local animation
+ local yawDif
+
+ //if ( IsAlive( player ) )
+ //{
+ // local table = GetFrontRightDots( titan, player )
+ //
+ // local dotForward = Table.dotForward
+ // local dotRight = Table.dotRight
+ //
+ //// DebugDrawLine( titanOrg, titanOrg + titan.GetForwardVector() * 200, 255, 0, 0, true, 5 )
+ //// DebugDrawLine( titanOrg, titanOrg + vecToEnt * 200, 0, 255, 0, true, 5 )
+ //
+ // if ( dotForward > 0.88 )
+ // {
+ // animation = "at_MP_stand2knee_L90"
+ // yawDif = 0
+ // }
+ // else
+ // if ( dotForward < -0.88 )
+ // {
+ // animation = "at_MP_stand2knee_R90"
+ // yawDif = 180
+ // }
+ // else
+ // if ( dotRight > 0 )
+ // {
+ // animation = "at_MP_stand2knee_straight"
+ // yawDif = 90
+ // }
+ // else
+ // {
+ // animation = "at_MP_stand2knee_180"
+ // yawDif = -90
+ // }
+ //}
+ //else
+ {
+ animation = "at_MP_stand2knee_straight"
+ yawDif = 0
+ }
+
+ thread HideOgreMainWeaponFromEnemies( titan )
+
+ if ( !IsAlive( player ) )
+ {
+ waitthread PlayAnimGravity( titan, animation )
+ return
+ }
+
+ local titanOrg = titan.GetOrigin()
+ local playerOrg = player.GetOrigin()
+
+ /*
+ local vec = playerOrg - titanOrg
+ vec.z = 0
+
+ local angles = VectorToAngles( vec )
+
+ angles.y += yawDif
+ */
+
+ local angles = titan.GetAngles()
+
+ titan.Anim_ScriptedPlayWithRefPoint( animation, titanOrg, angles, 0.5 )
+ titan.Anim_EnablePlanting()
+
+ WaittillAnimDone( titan )
+}
+
+function HideOgreMainWeaponFromEnemies( titan )
+{
+ expect entity( titan )
+
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ wait 1.0
+
+ entity soul = titan.GetTitanSoul()
+
+ Assert( IsValid( soul ) )
+
+ local titanSubClass = GetSoulTitanSubClass( soul )
+ if ( titanSubClass == "ogre" )
+ {
+ if ( IsValid( GetEnemyRodeoPilot( titan ) ) )
+ HideMainWeaponsFromEnemies( titan )
+ }
+}
+
+function HideMainWeaponsFromEnemies( titan )
+{
+ local weapons = titan.GetMainWeapons()
+ foreach ( weapon in weapons )
+ weapon.kv.visibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY
+}
+
+function ShowMainTitanWeapons( titan )
+{
+ local weapons = titan.GetMainWeapons()
+ foreach ( weapon in weapons )
+ {
+ weapon.kv.visibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ }
+}
+
+bool function ShouldBecomeAutoTitan( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+
+ if ( soul != null )
+ {
+ if ( SoulHasPassive( soul, ePassives.PAS_ENHANCED_TITAN_AI ) )
+ return true
+ }
+
+ return ( !PROTO_AutoTitansDisabled() )
+} \ No newline at end of file