diff options
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/ai')
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 00000000..da3058d7 --- /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 00000000..0429895b --- /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 00000000..e3addf81 --- /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 00000000..c0d56de7 --- /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 00000000..638166c8 --- /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 00000000..2f1fdc96 --- /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 00000000..771fe6d9 --- /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 00000000..e6d3bcf0 --- /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 00000000..588b4d75 --- /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 00000000..fc8b7d1e --- /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 00000000..4aa3ac30 --- /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 00000000..08598808 --- /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 00000000..0d4b43c9 --- /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 00000000..f1fbdb80 --- /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 00000000..3c2e36ce --- /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 00000000..37b89169 --- /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 00000000..9717c76d --- /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 00000000..37b89169 --- /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 00000000..6faf6649 --- /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 00000000..7e4d2cdd --- /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 00000000..c6e7f9f4 --- /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 00000000..214aff96 --- /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 00000000..f49560e0 --- /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 00000000..50b6cc75 --- /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 00000000..f8e0652c --- /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 00000000..eca5849b --- /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 00000000..e34b3082 --- /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 00000000..67c68600 --- /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 00000000..40a7d932 --- /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 00000000..b93631ac --- /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 00000000..f5c0c84d --- /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 00000000..9dbdd699 --- /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 00000000..347cb644 --- /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 |