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