aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_stalker.gnut606
1 files changed, 606 insertions, 0 deletions
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