From 207facbc402f5639cbcd31f079214351ef605cf2 Mon Sep 17 00:00:00 2001 From: BobTheBob <32057864+BobTheBob9@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:30:49 +0100 Subject: initial commit after moving to new repo --- .../scripts/vscripts/weapons/_weapon_utility.nut | 3966 ++++++++++++++++++++ 1 file changed, 3966 insertions(+) create mode 100644 Northstar.CustomServers/scripts/vscripts/weapons/_weapon_utility.nut (limited to 'Northstar.CustomServers/scripts/vscripts/weapons/_weapon_utility.nut') diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_weapon_utility.nut b/Northstar.CustomServers/scripts/vscripts/weapons/_weapon_utility.nut new file mode 100644 index 000000000..b3e5f5a39 --- /dev/null +++ b/Northstar.CustomServers/scripts/vscripts/weapons/_weapon_utility.nut @@ -0,0 +1,3966 @@ +untyped + +//TODO: Should split this up into server, client and shared versions and just globalize_all_functions +global function WeaponUtility_Init + +global function ApplyVectorSpread +global function DebugDrawMissilePath +global function DegreesToTarget +global function DetonateAllPlantedExplosives +global function EntityCanHaveStickyEnts +global function EntityShouldStick +global function FireExpandContractMissiles +global function FireExpandContractMissiles_S2S +global function GetVectorFromPositionToCrosshair +global function GetVelocityForDestOverTime +global function GetPlayerVelocityForDestOverTime +global function GetWeaponBurnMods +global function InitMissileForRandomDriftForVortexLow +global function IsPilotShotgunWeapon +global function PlantStickyEntity +global function PlantStickyEntityThatBouncesOffWalls +global function PlantStickyEntityOnWorldThatBouncesOffWalls +global function PlantStickyGrenade +global function PlantSuperStickyGrenade +global function Player_DetonateSatchels +global function PROTO_CanPlayerDeployWeapon +global function ProximityCharge_PostFired_Init +global function RegenerateOffhandAmmoOverTime +global function ShotgunBlast +global function FireGenericBoltWithDrop +global function OnWeaponPrimaryAttack_GenericBoltWithDrop_Player +global function OnWeaponPrimaryAttack_GenericMissile_Player +global function OnWeaponActivate_updateViewmodelAmmo +global function TEMP_GetDamageFlagsFromProjectile +global function WeaponCanCrit +global function GiveEMPStunStatusEffects +global function GetPrimaryWeapons +global function GetSidearmWeapons +global function GetATWeapons +global function GetPlayerFromTitanWeapon +global function ChargeBall_Precache +global function ChargeBall_FireProjectile +global function ChargeBall_ChargeBegin +global function ChargeBall_ChargeEnd +global function ChargeBall_StopChargeEffects +global function ChargeBall_GetChargeTime + +global function PlayerUsedOffhand +#if SERVER +global function SetPlayerCooldowns +global function ResetPlayerCooldowns +global function StoreOffhandData +#endif + +global function GetRadiusDamageDataFromProjectile + +#if DEV +global function DevPrintAllStatusEffectsOnEnt +#endif // #if DEV + +#if SERVER + global function ClusterRocket_Detonate + global function PassThroughDamage + global function PROTO_CleanupTrackedProjectiles + global function PROTO_InitTrackedProjectile + global function PROTO_PlayTrapLightEffect + global function Satchel_PostFired_Init + global function StartClusterExplosions + global function TrapDestroyOnRoundEnd + global function TrapExplodeOnDamage + global function PROTO_DelayCooldown + global function PROTO_FlakCannonMissiles + global function GetBulletPassThroughTargets + global function IsValidPassThroughTarget + global function GivePlayerAmpedWeapon + global function GivePlayerAmpedWeaponAndSetAsActive + global function ReplacePlayerOffhand + global function ReplacePlayerOrdnance + global function DisableWeapons + global function EnableWeapons + global function WeaponAttackWave + global function AddActiveThermiteBurn + global function GetActiveThermiteBurnsWithinRadius + global function OnWeaponPrimaryAttack_GenericBoltWithDrop_NPC + global function OnWeaponPrimaryAttack_GenericMissile_NPC + global function EMP_DamagedPlayerOrNPC + global function EMP_FX + global function GetWeaponDPS + global function GetTTK + global function GetWeaponModsFromDamageInfo + global function Thermite_DamagePlayerOrNPCSounds + global function AddThreatScopeColorStatusEffect + global function RemoveThreatScopeColorStatusEffect +#endif //SERVER +#if CLIENT + global function GlobalClientEventHandler + global function UpdateViewmodelAmmo + global function ServerCallback_AirburstIconUpdate + global function ServerCallback_GuidedMissileDestroyed + global function IsOwnerViewPlayerFullyADSed +#endif //CLIENT + +global const PROJECTILE_PREDICTED = true +global const PROJECTILE_NOT_PREDICTED = false + +global const PROJECTILE_LAG_COMPENSATED = true +global const PROJECTILE_NOT_LAG_COMPENSATED = false + +const float EMP_SEVERITY_SLOWTURN = 0.35 +const float EMP_SEVERITY_SLOWMOVE = 0.50 +const float LASER_STUN_SEVERITY_SLOWTURN = 0.20 +const float LASER_STUN_SEVERITY_SLOWMOVE = 0.30 + +const asset FX_EMP_BODY_HUMAN = $"P_emp_body_human" +const asset FX_EMP_BODY_TITAN = $"P_emp_body_titan" +const asset FX_VANGUARD_ENERGY_BODY_HUMAN = $"P_monarchBeam_body_human" +const asset FX_VANGUARD_ENERGY_BODY_TITAN = $"P_monarchBeam_body_titan" +const SOUND_EMP_REBOOT_SPARKS = "marvin_weld" +const FX_EMP_REBOOT_SPARKS = $"weld_spark_01_sparksfly" +const EMP_GRENADE_BEAM_EFFECT = $"wpn_arc_cannon_beam" +const DRONE_REBOOT_TIME = 5.0 +const GUNSHIP_REBOOT_TIME = 5.0 + +global struct RadiusDamageData +{ + int explosionDamage + int explosionDamageHeavyArmor + float explosionRadius + float explosionInnerRadius +} + +#if SERVER + +global struct PopcornInfo +{ + string weaponName + array weaponMods // could be array< string > + int damageSourceId + int count + float delay + float offset + float range + vector normal + float duration + int groupSize + bool hasBase +} + +struct ColorSwapStruct +{ + int statusEffectId + entity weaponOwner +} + +struct +{ + float titanRocketLauncherTitanDamageRadius + float titanRocketLauncherOtherDamageRadius + + int activeThermiteBurnsManagedEnts + array colorSwapStatusEffects +} file + +global int HOLO_PILOT_TRAIL_FX + +global struct HoverSounds +{ + string liftoff_1p + string liftoff_3p + string hover_1p + string hover_3p + string descent_1p + string descent_3p + string landing_1p + string landing_3p +} + +#endif + +function WeaponUtility_Init() +{ + level.weaponsPrecached <- {} + + // what classes can sticky thrown entities stick to? + level.stickyClasses <- {} + level.stickyClasses[ "worldspawn" ] <- true + level.stickyClasses[ "player" ] <- true + level.stickyClasses[ "prop_dynamic" ] <- true + level.stickyClasses[ "prop_script" ] <- true + level.stickyClasses[ "func_brush" ] <- true + level.stickyClasses[ "func_brush_lightweight" ] <- true + level.stickyClasses[ "phys_bone_follower" ] <- true + + level.trapChainReactClasses <- {} + level.trapChainReactClasses[ "mp_weapon_frag_grenade" ] <- true + level.trapChainReactClasses[ "mp_weapon_satchel" ] <- true + level.trapChainReactClasses[ "mp_weapon_proximity_mine" ] <- true + level.trapChainReactClasses[ "mp_weapon_laser_mine" ] <- true + + RegisterSignal( "Planted" ) + RegisterSignal( "EMP_FX" ) + RegisterSignal( "ArcStunned" ) + + PrecacheParticleSystem( EMP_GRENADE_BEAM_EFFECT ) + PrecacheParticleSystem( FX_EMP_BODY_TITAN ) + PrecacheParticleSystem( FX_EMP_BODY_HUMAN ) + PrecacheParticleSystem( FX_VANGUARD_ENERGY_BODY_HUMAN ) + PrecacheParticleSystem( FX_VANGUARD_ENERGY_BODY_TITAN ) + PrecacheParticleSystem( FX_EMP_REBOOT_SPARKS ) + + PrecacheImpactEffectTable( CLUSTER_ROCKET_FX_TABLE ) + + #if SERVER + AddDamageCallbackSourceID( eDamageSourceId.mp_titanweapon_triple_threat, TripleThreatGrenade_DamagedPlayerOrNPC ) + AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_defender, Defender_DamagedPlayerOrNPC ) + //AddDamageCallbackSourceID( eDamageSourceId.mp_titanweapon_rocketeer_rocketstream, TitanRocketLauncher_DamagedPlayerOrNPC ) + AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_smr, SMR_DamagedPlayerOrNPC ) + AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_flak_rifle, PROTO_Flak_Rifle_DamagedPlayerOrNPC ) + AddDamageCallbackSourceID( eDamageSourceId.mp_titanweapon_stun_laser, VanguardEnergySiphon_DamagedPlayerOrNPC ) + AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_grenade_emp, EMP_DamagedPlayerOrNPC ) + AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_proximity_mine, EMP_DamagedPlayerOrNPC ) + AddDamageCallbackSourceID( eDamageSourceId[ CHARGE_TOOL ], EMP_DamagedPlayerOrNPC ) + if ( IsMultiplayer() ) + AddCallback_OnPlayerRespawned( PROTO_TrackedProjectile_OnPlayerRespawned ) + AddCallback_OnPlayerKilled( PAS_CooldownReduction_OnKill ) + AddCallback_OnPlayerGetsNewPilotLoadout( OnPlayerGetsNewPilotLoadout ) + AddCallback_OnPlayerKilled( OnPlayerKilled ) + + file.activeThermiteBurnsManagedEnts = CreateScriptManagedEntArray() + + AddCallback_EntitiesDidLoad( EntitiesDidLoad ) + + HOLO_PILOT_TRAIL_FX = PrecacheParticleSystem( $"P_ar_holopilot_trail" ) + + PrecacheParticleSystem( $"wpn_laser_blink" ) + PrecacheParticleSystem( $"wpn_laser_blink_fast" ) + PrecacheParticleSystem( $"P_ordinance_icon_owner" ) + #endif +} + +#if SERVER +void function EntitiesDidLoad() +{ +#if SP + // if we are going to do this, it should happen in the weapon, not globally + //float titanRocketLauncherInnerRadius = expect float( GetWeaponInfoFileKeyField_Global( "mp_titanweapon_rocketeer_rocketstream", "explosion_inner_radius" ) ) + //float titanRocketLauncherOuterRadius = expect float( GetWeaponInfoFileKeyField_Global( "mp_titanweapon_rocketeer_rocketstream", "explosionradius" ) ) + //file.titanRocketLauncherTitanDamageRadius = titanRocketLauncherInnerRadius + ( ( titanRocketLauncherOuterRadius - titanRocketLauncherInnerRadius ) * 0.4 ) + //file.titanRocketLauncherOtherDamageRadius = titanRocketLauncherInnerRadius + ( ( titanRocketLauncherOuterRadius - titanRocketLauncherInnerRadius ) * 0.1 ) +#endif +} +#endif + +//////////////////////////////////////////////////////////////////// + +#if CLIENT +void function GlobalClientEventHandler( entity weapon, string name ) +{ + if ( name == "ammo_update" ) + UpdateViewmodelAmmo( false, weapon ) + + if ( name == "ammo_full" ) + UpdateViewmodelAmmo( true, weapon ) +} + +function UpdateViewmodelAmmo( bool forceFull, entity weapon ) +{ + Assert( weapon != null ) // used to be: if ( weapon == null ) weapon = this.self + + if ( !IsValid( weapon ) ) + return + if ( !IsLocalViewPlayer( weapon.GetWeaponOwner() ) ) + return + + int bodyGroupCount = weapon.GetWeaponSettingInt( eWeaponVar.bodygroup_ammo_index_count ) + if ( bodyGroupCount <= 0 ) + return + + int rounds = weapon.GetWeaponPrimaryClipCount() + int maxRoundsForClipSize = weapon.GetWeaponPrimaryClipCountMax() + int maxRoundsForBodyGroup = (bodyGroupCount - 1) + int maxRounds = minint( maxRoundsForClipSize, maxRoundsForBodyGroup ) + + if ( forceFull || (rounds > maxRounds) ) + rounds = maxRounds + + //printt( "ROUNDS:", rounds, "/", maxRounds ) + weapon.SetViewmodelAmmoModelIndex( rounds ) +} +#endif // #if CLIENT + +void function OnWeaponActivate_updateViewmodelAmmo( entity weapon ) +{ +#if CLIENT + UpdateViewmodelAmmo( false, weapon ) +#endif // #if CLIENT +} + +#if SERVER +//////////////////WEAPON DAMAGE CALLBACKS///////////////////////////////////////// +void function TripleThreatGrenade_DamagedPlayerOrNPC( entity ent, var damageInfo ) +{ + if ( !IsValid( ent ) ) + return + + if ( ent.GetClassName() == "grenade_frag" ) + return + + if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS ) + return + + vector damagePosition = DamageInfo_GetDamagePosition( damageInfo ) + + vector entOrigin = ent.GetOrigin() + vector entCenter = ent.GetWorldSpaceCenter() + float distanceToOrigin = Distance( entOrigin, damagePosition ) + float distanceToCenter = Distance( entCenter, damagePosition ) + + vector normal = Vector( 0, 0, 1 ) + entity inflictor = DamageInfo_GetInflictor( damageInfo ) + if ( IsValid( inflictor.s ) ) + { + if ( "collisionNormal" in inflictor.s ) + normal = expect vector( inflictor.s.collisionNormal ) + } + + local zDifferenceOrigin = deg_cos( DegreesToTarget( entOrigin, normal, damagePosition ) ) * distanceToOrigin + local zDifferenceTop = deg_cos( DegreesToTarget( entCenter, normal, damagePosition ) ) * distanceToCenter - (entCenter.z - entOrigin.z) + + float zDamageDiff + //Full damage if explosion is between Origin or Center. + if ( zDifferenceOrigin > 0 && zDifferenceTop < 0 ) + zDamageDiff = 1.0 + else if ( zDifferenceTop > 0 ) + zDamageDiff = GraphCapped( zDifferenceTop, 0.0, 32.0, 1.0, 0.0 ) + else + zDamageDiff = GraphCapped( zDifferenceOrigin, 0.0, -32.0, 1.0, 0.0 ) + + DamageInfo_ScaleDamage( damageInfo, zDamageDiff ) +} + +void function Defender_DamagedPlayerOrNPC( entity ent, var damageInfo ) +{ + if ( !IsValid( ent ) ) + return + + if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS ) + return + + local damage = Vortex_HandleElectricDamage( ent, DamageInfo_GetAttacker( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetWeapon( damageInfo ) ) + DamageInfo_SetDamage( damageInfo, damage ) +} + +/* +void function TitanRocketLauncher_DamagedPlayerOrNPC( entity ent, var damageInfo ) +{ + Assert( IsSingleplayer() ) + + if ( !IsValid( ent ) ) + return + + if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS ) + return + + vector damagePosition = DamageInfo_GetDamagePosition( damageInfo ) + + if ( ent == DamageInfo_GetAttacker( damageInfo ) ) + return + + if ( ent.IsTitan() ) + { + vector entOrigin = ent.GetOrigin() + if ( Distance( damagePosition, entOrigin ) > file.titanRocketLauncherTitanDamageRadius ) + DamageInfo_SetDamage( damageInfo, 0 ) + } + else if ( IsHumanSized( ent ) ) + { + if ( Distance( damagePosition, ent.GetOrigin() ) > file.titanRocketLauncherOtherDamageRadius ) + DamageInfo_SetDamage( damageInfo, 0 ) + } +} +*/ + +void function SMR_DamagedPlayerOrNPC( entity ent, var damageInfo ) +{ + //Hack - JFS ( The explosion radius is too small on the SMR to deal splash damage to pilots on a Titan. ) + if ( !IsValid( ent ) ) + return + + if ( !ent.IsTitan() ) + return + + if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS ) + return + + entity attacker = DamageInfo_GetAttacker( damageInfo ) + + if ( IsValid( attacker ) && attacker.IsPlayer() && attacker.GetTitanSoulBeingRodeoed() == ent.GetTitanSoul() ) + attacker.TakeDamage( 30, attacker, attacker, { scriptType = DF_GIB | DF_EXPLOSION, damageSourceId = eDamageSourceId.mp_weapon_smr, weapon = DamageInfo_GetWeapon( damageInfo ) } ) +} + + +void function PROTO_Flak_Rifle_DamagedPlayerOrNPC( entity ent, var damageInfo ) +{ + entity attacker = DamageInfo_GetAttacker( damageInfo ) + + if ( !IsValid( ent ) || !IsValid( attacker ) ) + return + + if ( attacker == ent ) + DamageInfo_ScaleDamage( damageInfo, 0.5 ) +} + +function EngineerRocket_DamagedPlayerOrNPC( ent, damageInfo ) +{ + expect entity( ent ) + + entity attacker = DamageInfo_GetAttacker( damageInfo ) + + if ( !IsValid( ent ) || !IsValid( attacker ) ) + return + + if ( attacker == ent ) + DamageInfo_SetDamage( damageInfo, 10 ) +} +/////////////////////////////////////////////////////////////////////// + +#endif // SERVER + +vector function ApplyVectorSpread( vector vecShotDirection, float spreadDegrees, float bias = 1.0 ) +{ + vector angles = VectorToAngles( vecShotDirection ) + vector vecUp = AnglesToUp( angles ) + vector vecRight = AnglesToRight( angles ) + + float sinDeg = deg_sin( spreadDegrees / 2.0 ) + + // get circular gaussian spread + float x + float y + float z + + if ( bias > 1.0 ) + bias = 1.0 + else if ( bias < 0.0 ) + bias = 0.0 + + // code gets these values from cvars ai_shot_bias_min & ai_shot_bias_max + float shotBiasMin = -1.0 + float shotBiasMax = 1.0 + + // 1.0 gaussian, 0.0 is flat, -1.0 is inverse gaussian + float shotBias = ( ( shotBiasMax - shotBiasMin ) * bias ) + shotBiasMin + float flatness = ( fabs(shotBias) * 0.5 ) + + while ( true ) + { + x = RandomFloatRange( -1.0, 1.0 ) * flatness + RandomFloatRange( -1.0, 1.0 ) * (1 - flatness) + y = RandomFloatRange( -1.0, 1.0 ) * flatness + RandomFloatRange( -1.0, 1.0 ) * (1 - flatness) + if ( shotBias < 0 ) + { + x = ( x >= 0 ) ? 1.0 - x : -1.0 - x + y = ( y >= 0 ) ? 1.0 - y : -1.0 - y + } + z = x * x + y * y + + if ( z <= 1 ) + break + } + + vector addX = vecRight * ( x * sinDeg ) + vector addY = vecUp * ( y * sinDeg ) + vector m_vecResult = vecShotDirection + addX + addY + + return m_vecResult +} + + +float function DegreesToTarget( vector origin, vector forward, vector targetPos ) +{ + vector dirToTarget = targetPos - origin + dirToTarget = Normalize( dirToTarget ) + float dot = DotProduct( forward, dirToTarget ) + float degToTarget = (acos( dot ) * 180 / PI) + + return degToTarget +} + +function ShotgunBlast( entity weapon, vector pos, vector dir, int numBlasts, int damageType, float damageScaler = 1.0, float ornull maxAngle = null, float ornull maxDistance = null ) +{ + Assert( numBlasts > 0 ) + int numBlastsOriginal = numBlasts + entity owner = weapon.GetWeaponOwner() + + /* + Debug ConVars: + visible_ent_cone_debug_duration_client - Set to non-zero to see debug output + visible_ent_cone_debug_duration_server - Set to non-zero to see debug output + visible_ent_cone_debug_draw_radius - Size of trace endpoint debug draw + */ + + if ( maxDistance == null ) + maxDistance = weapon.GetMaxDamageFarDist() + expect float( maxDistance ) + + if ( maxAngle == null ) + maxAngle = owner.GetAttackSpreadAngle() * 0.5 + expect float( maxAngle ) + + array ignoredEntities = [ owner ] + int traceMask = TRACE_MASK_SHOT + int visConeFlags = VIS_CONE_ENTS_TEST_HITBOXES | VIS_CONE_ENTS_CHECK_SOLID_BODY_HIT | VIS_CONE_ENTS_APPOX_CLOSEST_HITBOX | VIS_CONE_RETURN_HIT_VORTEX + + entity antilagPlayer + if ( owner.IsPlayer() ) + { + if ( owner.IsPhaseShifted() ) + return; + + antilagPlayer = owner + } + + //JFS - Bug 198500 + Assert( maxAngle > 0.0, "JFS returning out at this instance. We need to investigate when a valid mp_titanweapon_laser_lite weapon returns 0 spread") + if ( maxAngle == 0.0 ) + return + + array results = FindVisibleEntitiesInCone( pos, dir, maxDistance, (maxAngle * 1.1), ignoredEntities, traceMask, visConeFlags, antilagPlayer, weapon ) + foreach ( result in results ) + { + float angleToHitbox = 0.0 + if ( !result.solidBodyHit ) + angleToHitbox = DegreesToTarget( pos, dir, result.approxClosestHitboxPos ) + + numBlasts -= ShotgunBlastDamageEntity( weapon, pos, dir, result, angleToHitbox, maxAngle, numBlasts, damageType, damageScaler ) + if ( numBlasts <= 0 ) + break + } + + //Something in the TakeDamage above is triggering the weapon owner to become invalid. + owner = weapon.GetWeaponOwner() + if ( !IsValid( owner ) ) + return + + // maxTracer limit set in /r1dev/src/game/client/c_player.h + const int MAX_TRACERS = 16 + bool didHitAnything = ((numBlastsOriginal - numBlasts) != 0) + bool doTraceBrushOnly = (!didHitAnything) + if ( numBlasts > 0 ) + weapon.FireWeaponBullet_Special( pos, dir, minint( numBlasts, MAX_TRACERS ), damageType, false, false, true, false, false, false, doTraceBrushOnly ) +} + + +const SHOTGUN_ANGLE_MIN_FRACTION = 0.1; +const SHOTGUN_ANGLE_MAX_FRACTION = 1.0; +const SHOTGUN_DAMAGE_SCALE_AT_MIN_ANGLE = 0.8; +const SHOTGUN_DAMAGE_SCALE_AT_MAX_ANGLE = 0.1; + +int function ShotgunBlastDamageEntity( entity weapon, vector barrelPos, vector barrelVec, VisibleEntityInCone result, float angle, float maxAngle, int numPellets, int damageType, float damageScaler ) +{ + entity target = result.ent + + //The damage scaler is currently only > 1 for the Titan Shotgun alt fire. + if ( !target.IsTitan() && damageScaler > 1 ) + damageScaler = max( damageScaler * 0.4, 1.5 ) + + entity owner = weapon.GetWeaponOwner() + // Ent in cone not valid + if ( !IsValid( target ) || !IsValid( owner ) ) + return 0 + + // Fire fake bullet towards entity for visual purposes only + vector hitLocation = result.visiblePosition + vector vecToEnt = ( hitLocation - barrelPos ) + vecToEnt.Norm() + if ( Length( vecToEnt ) == 0 ) + vecToEnt = barrelVec + + // This fires a fake bullet that doesn't do any damage. Currently it triggeres a damage callback with 0 damage which is bad. + weapon.FireWeaponBullet_Special( barrelPos, vecToEnt, 1, damageType, true, true, true, false, false, false, false ) // fires perfect bullet with no antilag and no spread + +#if SERVER + // Determine how much damage to do based on distance + float distanceToTarget = Distance( barrelPos, hitLocation ) + + if ( !result.solidBodyHit ) // non solid hits take 1 blast more + distanceToTarget += 130 + + int extraMods = result.extraMods + float damageAmount = CalcWeaponDamage( owner, target, weapon, distanceToTarget, extraMods ) + + // vortex needs to scale damage based on number of rounds absorbed + string className = weapon.GetWeaponClassName() + if ( (className == "mp_titanweapon_vortex_shield") || (className == "mp_titanweapon_vortex_shield_ion") || (className == "mp_titanweapon_heat_shield") ) + { + damageAmount *= numPellets + //printt( "scaling vortex hitscan output damage by", numPellets, "pellets for", weaponNearDamageTitan, "damage vs titans" ) + } + + float coneScaler = 1.0 + //if ( angle > 0 ) + // coneScaler = GraphCapped( angle, (maxAngle * SHOTGUN_ANGLE_MIN_FRACTION), (maxAngle * SHOTGUN_ANGLE_MAX_FRACTION), SHOTGUN_DAMAGE_SCALE_AT_MIN_ANGLE, SHOTGUN_DAMAGE_SCALE_AT_MAX_ANGLE ) + + // Calculate the final damage abount to inflict on the target. Also scale it by damageScaler which may have been passed in by script ( used by alt fire mode on titan shotgun to fire multiple shells ) + float finalDamageAmount = damageAmount * coneScaler * damageScaler + //printt( "angle:", angle, "- coneScaler:", coneScaler, "- damageAmount:", damageAmount, "- damageScaler:", damageScaler, " = finalDamageAmount:", finalDamageAmount ) + + // Calculate impulse force to apply based on damage + int maxImpulseForce = expect int( weapon.GetWeaponInfoFileKeyField( "impulse_force" ) ) + float impulseForce = float( maxImpulseForce ) * coneScaler * damageScaler + vector impulseVec = barrelVec * impulseForce + + int damageSourceID = weapon.GetDamageSourceID() + + // + float critScale = weapon.GetWeaponSettingFloat( eWeaponVar.critical_hit_damage_scale ) + target.TakeDamage( finalDamageAmount, owner, weapon, { origin = hitLocation, force = impulseVec, scriptType = damageType, damageSourceId = damageSourceID, weapon = weapon, hitbox = result.visibleHitbox, criticalHitScale = critScale } ) + + //printt( "-----------" ) + //printt( " distanceToTarget:", distanceToTarget ) + //printt( " damageAmount:", damageAmount ) + //printt( " coneScaler:", coneScaler ) + //printt( " impulseForce:", impulseForce ) + //printt( " impulseVec:", impulseVec.x + ", " + impulseVec.y + ", " + impulseVec.z ) + //printt( " finalDamageAmount:", finalDamageAmount ) + //PrintTable( result ) +#endif // #if SERVER + + return 1 +} + +int function FireGenericBoltWithDrop( entity weapon, WeaponPrimaryAttackParams attackParams, bool isPlayerFired ) +{ +#if CLIENT + if ( !weapon.ShouldPredictProjectiles() ) + return 1 +#endif // #if CLIENT + + weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 ) + + const float PROJ_SPEED_SCALE = 1 + const float PROJ_GRAVITY = 1 + int damageFlags = weapon.GetWeaponDamageFlags() + entity bolt = weapon.FireWeaponBolt( attackParams.pos, attackParams.dir, PROJ_SPEED_SCALE, damageFlags, damageFlags, isPlayerFired, 0 ) + if ( bolt != null ) + { + bolt.kv.gravity = PROJ_GRAVITY + bolt.kv.rendercolor = "0 0 0" + bolt.kv.renderamt = 0 + bolt.kv.fadedist = 1 + } + + return 1 +} +var function OnWeaponPrimaryAttack_GenericBoltWithDrop_Player( entity weapon, WeaponPrimaryAttackParams attackParams ) +{ + return FireGenericBoltWithDrop( weapon, attackParams, true ) +} + +var function OnWeaponPrimaryAttack_EPG( entity weapon, WeaponPrimaryAttackParams attackParams ) +{ + entity missile = weapon.FireWeaponMissile( attackParams.pos, attackParams.dir, 1, damageTypes.largeCaliberExp, damageTypes.largeCaliberExp, false, PROJECTILE_NOT_PREDICTED ) + if ( missile ) + { + EmitSoundOnEntity( missile, "Weapon_Sidwinder_Projectile" ) + missile.InitMissileForRandomDriftFromWeaponSettings( attackParams.pos, attackParams.dir ) + } + + return missile +} + +#if SERVER +var function OnWeaponPrimaryAttack_GenericBoltWithDrop_NPC( entity weapon, WeaponPrimaryAttackParams attackParams ) +{ + return FireGenericBoltWithDrop( weapon, attackParams, false ) +} +#endif // #if SERVER + + +var function OnWeaponPrimaryAttack_GenericMissile_Player( entity weapon, WeaponPrimaryAttackParams attackParams ) +{ + weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 ) + + entity weaponOwner = weapon.GetWeaponOwner() + vector bulletVec = ApplyVectorSpread( attackParams.dir, weaponOwner.GetAttackSpreadAngle() - 1.0 ) + attackParams.dir = bulletVec + + if ( IsServer() || weapon.ShouldPredictProjectiles() ) + { + entity missile = weapon.FireWeaponMissile( attackParams.pos, attackParams.dir, 1.0, weapon.GetWeaponDamageFlags(), weapon.GetWeaponDamageFlags(), false, PROJECTILE_PREDICTED ) + if ( missile ) + { + missile.InitMissileForRandomDriftFromWeaponSettings( attackParams.pos, attackParams.dir ) + } + } +} + +#if SERVER +var function OnWeaponPrimaryAttack_GenericMissile_NPC( entity weapon, WeaponPrimaryAttackParams attackParams ) +{ + weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 ) + + entity missile = weapon.FireWeaponMissile( attackParams.pos, attackParams.dir, 1.0, weapon.GetWeaponDamageFlags(), weapon.GetWeaponDamageFlags(), true, PROJECTILE_NOT_PREDICTED ) + if ( missile ) + { + missile.InitMissileForRandomDriftFromWeaponSettings( attackParams.pos, attackParams.dir ) + } +} +#endif // #if SERVER + +bool function PlantStickyEntityOnWorldThatBouncesOffWalls( entity ent, table collisionParams, float bounceDot ) +{ + entity hitEnt = expect entity( collisionParams.hitEnt ) + if ( hitEnt && ( hitEnt.IsWorld() || hitEnt.HasPusherRootParent() ) ) + { + float dot = expect vector( collisionParams.normal ).Dot( Vector( 0, 0, 1 ) ) + + if ( dot < bounceDot ) + return false + + return PlantStickyEntity( ent, collisionParams ) + } + + return false +} + +bool function PlantStickyEntityThatBouncesOffWalls( entity ent, table collisionParams, float bounceDot ) +{ + if ( expect entity( collisionParams.hitEnt ) == GetEntByIndex( 0 ) ) + { + // Satchel hit the world + float dot = expect vector( collisionParams.normal ).Dot( Vector( 0, 0, 1 ) ) + + if ( dot < bounceDot ) + return false + } + + return PlantStickyEntity( ent, collisionParams ) +} + + +bool function PlantStickyEntity( entity ent, table collisionParams, vector angleOffset = <0.0, 0.0, 0.0> ) +{ + if ( !EntityShouldStick( ent, expect entity( collisionParams.hitEnt ) ) ) + return false + + // Don't allow parenting to another "sticky" entity to prevent them parenting onto each other + if ( collisionParams.hitEnt.IsProjectile() ) + return false + + // Update normal from last bouce so when it explodes it can orient the effect properly + + vector plantAngles = AnglesCompose( VectorToAngles( collisionParams.normal ), angleOffset ) + vector plantPosition = expect vector( collisionParams.pos ) + + if ( !LegalOrigin( plantPosition ) ) + return false + + #if SERVER + ent.SetAbsOrigin( plantPosition ) + ent.SetAbsAngles( plantAngles ) + ent.proj.isPlanted = true + #else + ent.SetOrigin( plantPosition ) + ent.SetAngles( plantAngles ) + #endif + ent.SetVelocity( Vector( 0, 0, 0 ) ) + + //printt( " - Hitbox is:", collisionParams.hitbox, " IsWorld:", collisionParams.hitEnt ) + if ( !collisionParams.hitEnt.IsWorld() ) + { + if ( !ent.IsMarkedForDeletion() && !collisionParams.hitEnt.IsMarkedForDeletion() ) + { + if ( collisionParams.hitbox > 0 ) + ent.SetParentWithHitbox( collisionParams.hitEnt, collisionParams.hitbox, true ) + + // Hit a func_brush + else + ent.SetParent( collisionParams.hitEnt ) + + if ( collisionParams.hitEnt.IsPlayer() ) + { + thread HandleDisappearingParent( ent, expect entity( collisionParams.hitEnt ) ) + } + } + } + else + { + ent.SetVelocity( Vector( 0, 0, 0 ) ) + ent.StopPhysics() + } + #if CLIENT + if ( ent instanceof C_BaseGrenade ) + #else + if ( ent instanceof CBaseGrenade ) + #endif + ent.MarkAsAttached() + + ent.Signal( "Planted" ) + + return true +} + +bool function PlantStickyGrenade( entity ent, vector pos, vector normal, entity hitEnt, int hitbox, float depth = 0.0, bool allowBounce = true, bool allowEntityStick = true ) +{ + if ( ent.GetTeam() == hitEnt.GetTeam() ) + return false + + if ( ent.IsMarkedForDeletion() || hitEnt.IsMarkedForDeletion() ) + return false + + vector plantAngles = VectorToAngles( normal ) + vector plantPosition = pos + normal * -depth + + if ( !allowBounce ) + ent.SetVelocity( Vector( 0, 0, 0 ) ) + + if ( !LegalOrigin( plantPosition ) ) + return false + + #if SERVER + ent.SetAbsOrigin( plantPosition ) + ent.SetAbsAngles( plantAngles ) + ent.proj.isPlanted = true + #else + ent.SetOrigin( plantPosition ) + ent.SetAngles( plantAngles ) + #endif + + if ( !hitEnt.IsWorld() && (!hitEnt.IsTitan() || !allowEntityStick) ) + return false + + // SetOrigin might be causing the ent to get markedForDeletion. + if ( ent.IsMarkedForDeletion() ) + return false + + ent.SetVelocity( Vector( 0, 0, 0 ) ) + + if ( hitEnt.IsWorld() ) + { + ent.SetParent( hitEnt, "", true ) + ent.StopPhysics() + } + else + { + if ( hitbox > 0 ) + ent.SetParentWithHitbox( hitEnt, hitbox, true ) + else // Hit a func_brush + ent.SetParent( hitEnt ) + + if ( hitEnt.IsPlayer() ) + { + thread HandleDisappearingParent( ent, hitEnt ) + } + } + + #if CLIENT + if ( ent instanceof C_BaseGrenade ) + ent.MarkAsAttached() + #else + if ( ent instanceof CBaseGrenade ) + ent.MarkAsAttached() + #endif + + return true +} + + +bool function PlantSuperStickyGrenade( entity ent, vector pos, vector normal, entity hitEnt, int hitbox ) +{ + if ( ent.GetTeam() == hitEnt.GetTeam() ) + return false + + vector plantAngles = VectorToAngles( normal ) + vector plantPosition = pos + + if ( !LegalOrigin( plantPosition ) ) + return false + + #if SERVER + ent.SetAbsOrigin( plantPosition ) + ent.SetAbsAngles( plantAngles ) + ent.proj.isPlanted = true + #else + ent.SetOrigin( plantPosition ) + ent.SetAngles( plantAngles ) + #endif + + if ( !hitEnt.IsWorld() && !hitEnt.IsPlayer() && !hitEnt.IsNPC() ) + return false + + ent.SetVelocity( Vector( 0, 0, 0 ) ) + + if ( hitEnt.IsWorld() ) + { + ent.StopPhysics() + } + else + { + if ( !ent.IsMarkedForDeletion() && !hitEnt.IsMarkedForDeletion() ) + { + if ( hitbox > 0 ) + ent.SetParentWithHitbox( hitEnt, hitbox, true ) + else // Hit a func_brush + ent.SetParent( hitEnt ) + + if ( hitEnt.IsPlayer() ) + { + thread HandleDisappearingParent( ent, hitEnt ) + } + } + } + + #if CLIENT + if ( ent instanceof C_BaseGrenade ) + ent.MarkAsAttached() + #else + if ( ent instanceof CBaseGrenade ) + ent.MarkAsAttached() + #endif + + return true +} + +#if SERVER +void function HandleDisappearingParent( entity ent, entity parentEnt ) +{ + parentEnt.EndSignal( "OnDeath" ) + ent.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( ent ) + { + ent.ClearParent() + } + ) + + parentEnt.WaitSignal( "StartPhaseShift" ) +} +#else +void function HandleDisappearingParent( entity ent, entity parentEnt ) +{ + parentEnt.EndSignal( "OnDeath" ) + ent.EndSignal( "OnDestroy" ) + + parentEnt.WaitSignal( "StartPhaseShift" ) + + ent.ClearParent() +} +#endif + +bool function EntityShouldStick( entity stickyEnt, entity hitent ) +{ + if ( !EntityCanHaveStickyEnts( stickyEnt, hitent ) ) + return false + + if ( hitent == stickyEnt ) + return false + + return true +} + +bool function EntityCanHaveStickyEnts( entity stickyEnt, entity ent ) +{ + if ( !IsValid( ent ) ) + return false + + if ( ent.GetModelName() == $"" ) // valid case, other projectiles bullets, etc.. sometimes have no model + return false; + + local entClassname + if ( IsServer() ) + entClassname = ent.GetClassName() + else + entClassname = ent.GetSignifierName() // Can return null + + if ( !( entClassname in level.stickyClasses ) && !ent.IsNPC() ) + return false + + #if CLIENT + if ( stickyEnt instanceof C_Projectile ) + #else + if ( stickyEnt instanceof CProjectile ) + #endif + { + string weaponClassName = stickyEnt.ProjectileGetWeaponClassName() + local stickPlayer = GetWeaponInfoFileKeyField_Global( weaponClassName, "stick_pilot" ) + local stickTitan = GetWeaponInfoFileKeyField_Global( weaponClassName, "stick_titan" ) + local stickNPC = GetWeaponInfoFileKeyField_Global( weaponClassName, "stick_npc" ) + + if ( ent.IsTitan() && stickTitan ) + return true + else if ( ent.IsPlayer() && stickPlayer ) + return true + else if ( ent.IsNPC() && stickNPC ) + return true + + // not pilots + if ( ent.IsPlayer() && !ent.IsTitan() ) + return false + } + + return true +} + +#if SERVER +// shared with the vortex script which also needs to create satchels +function Satchel_PostFired_Init( entity satchel, entity player ) +{ + satchel.proj.onlyAllowSmartPistolDamage = false + thread SatchelThink( satchel, player ) +} + +function SatchelThink( entity satchel, entity player ) +{ + player.EndSignal("OnDestroy") + satchel.EndSignal("OnDestroy") + + int satchelHealth = 15 + thread TrapExplodeOnDamage( satchel, satchelHealth ) + + #if DEV + // temp HACK for FX to use to figure out the size of the particle to play + if ( Flag( "ShowExplosionRadius" ) ) + thread ShowExplosionRadiusOnExplode( satchel ) + #endif + + player.EndSignal( "OnDeath" ) + + OnThreadEnd( + function() : ( satchel ) + { + if ( IsValid( satchel ) ) + { + satchel.Destroy() + } + } + ) + + WaitForever() +} + +#endif // SERVER + +function ProximityCharge_PostFired_Init( entity proximityMine, entity player ) +{ + #if SERVER + proximityMine.proj.onlyAllowSmartPistolDamage = false + #endif +} + +function DetonateAllPlantedExplosives( entity player ) +{ + // ToDo: Could use Player_DetonateSatchels but it only tracks satchels, not laser mines. + + // Detonate all explosives - satchels and laser mines are also frag grenades in disguise + array grenades = GetProjectileArrayEx( "grenade_frag", TEAM_ANY, TEAM_ANY, Vector( 0, 0, 0 ), -1 ) + foreach( grenade in grenades ) + { + if ( grenade.GetOwner() != player ) + continue + + if ( grenade.ProjectileGetDamageSourceID() != eDamageSourceId.mp_weapon_satchel && grenade.ProjectileGetDamageSourceID() != eDamageSourceId.mp_weapon_proximity_mine ) + continue + + thread ExplodePlantedGrenadeAfterDelay( grenade, RandomFloatRange( 0.75, 0.95 ) ) + } +} + +function ExplodePlantedGrenadeAfterDelay( entity grenade, float delay ) +{ + grenade.EndSignal( "OnDeath" ) + grenade.EndSignal( "OnDestroy" ) + + float endTime = Time() + delay + + while ( Time() < endTime ) + { + EmitSoundOnEntity( grenade, DEFAULT_WARNING_SFX ) + wait 0.1 + } + + grenade.GrenadeExplode( grenade.GetForwardVector() ) +} + +function Player_DetonateSatchels( entity player ) +{ + #if SERVER + Assert( IsServer() ) + + array traps = GetScriptManagedEntArray( player.s.activeTrapArrayId ) + traps.sort( CompareCreationReverse ) + foreach ( index, satchel in traps ) + { + if ( IsValidSatchel( satchel ) ) + { + + thread PROTO_ExplodeAfterDelay( satchel, index * 0.25 ) + } + } + #endif +} + +function IsValidSatchel( entity satchel ) +{ + #if SERVER + if ( satchel.ProjectileGetWeaponClassName() != "mp_weapon_satchel" ) + return false + + if ( satchel.e.isDisabled == true ) + return false + + return true + #endif +} + +#if SERVER +function PROTO_ExplodeAfterDelay( entity satchel, float delay ) +{ + satchel.EndSignal( "OnDestroy" ) + + #if MP + while ( !satchel.proj.isPlanted ) + { + WaitFrame() + } + #endif + + wait delay + + satchel.GrenadeExplode( satchel.GetForwardVector() ) +} +#endif + + +#if DEV +function ShowExplosionRadiusOnExplode( entity ent ) +{ + ent.WaitSignal( "OnDestroy" ) + + float innerRadius = expect float( ent.GetWeaponInfoFileKeyField( "explosion_inner_radius" ) ) + float outerRadius = expect float( ent.GetWeaponInfoFileKeyField( "explosionradius" ) ) + + vector org = ent.GetOrigin() + vector angles = Vector( 0, 0, 0 ) + thread DebugDrawCircle( org, angles, innerRadius, 255, 255, 51, true, 3.0 ) + thread DebugDrawCircle( org, angles, outerRadius, 255, 255, 255, true, 3.0 ) +} +#endif // DEV + +#if SERVER +// shared between nades, satchels and laser mines +void function TrapExplodeOnDamage( entity trapEnt, int trapEntHealth = 50, float waitMin = 0.0, float waitMax = 0.0 ) +{ + Assert( IsValid( trapEnt ), "Given trapEnt entity is not valid, fired from: " + trapEnt.ProjectileGetWeaponClassName() ) + EndSignal( trapEnt, "OnDestroy" ) + + trapEnt.SetDamageNotifications( true ) + var results //Really should be a struct + entity attacker + entity inflictor + + while ( true ) + { + if ( !IsValid( trapEnt ) ) + return + + results = WaitSignal( trapEnt, "OnDamaged" ) + attacker = expect entity( results.activator ) + inflictor = expect entity( results.inflictor ) + + if ( IsValid( inflictor ) && inflictor == trapEnt ) + continue + + bool shouldDamageTrap = false + if ( IsValid( attacker ) ) + { + if ( trapEnt.proj.onlyAllowSmartPistolDamage ) + { + if ( attacker.IsNPC() || attacker.IsPlayer() ) + { + entity attackerWeapon = attacker.GetActiveWeapon() + if ( IsValid( attackerWeapon ) && WeaponIsSmartPistolVariant( attackerWeapon ) ) + shouldDamageTrap = true + } + } + else + { + if ( trapEnt.GetTeam() == attacker.GetTeam() ) + { + if ( trapEnt.GetOwner() != attacker ) + shouldDamageTrap = false + else + shouldDamageTrap = !ProjectileIgnoresOwnerDamage( trapEnt ) + } + else + { + shouldDamageTrap = true + } + } + } + + if ( shouldDamageTrap ) + trapEntHealth -= int ( results.value ) //TODO: This returns float even though it feels like it should return int + + if ( trapEntHealth <= 0 ) + break + } + + if ( !IsValid( trapEnt ) ) + return + + inflictor = expect entity( results.inflictor ) // waiting on code feature to pass inflictor with OnDamaged signal results table + + if ( waitMin >= 0 && waitMax > 0 ) + { + float waitTime = RandomFloatRange( waitMin, waitMax ) + + if ( waitTime > 0 ) + wait waitTime + } + else if ( IsValid( inflictor ) && (inflictor.IsProjectile() || (inflictor instanceof CWeaponX)) ) + { + int dmgSourceID + if ( inflictor.IsProjectile() ) + dmgSourceID = inflictor.ProjectileGetDamageSourceID() + else + dmgSourceID = inflictor.GetDamageSourceID() + + string inflictorClass = GetObitFromDamageSourceID( dmgSourceID ) + + if ( inflictorClass in level.trapChainReactClasses ) + { + // chain reaction delay + Wait( RandomFloatRange( 0.2, 0.275 ) ) + } + } + + if ( !IsValid( trapEnt ) ) + return + + if ( IsValid( attacker ) ) + { + if ( attacker.IsPlayer() ) + { + AddPlayerScoreForTrapDestruction( attacker, trapEnt ) + trapEnt.SetOwner( attacker ) + } + else + { + entity lastAttacker = GetLastAttacker( attacker ) + if ( IsValid( lastAttacker ) ) + { + // for chain explosions, figure out the attacking player that started the chain + trapEnt.SetOwner( lastAttacker ) + } + } + } + + trapEnt.GrenadeExplode( trapEnt.GetForwardVector() ) +} + +bool function ProjectileIgnoresOwnerDamage( entity projectile ) +{ + var ignoreOwnerDamage = projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_ignore_owner_damage" ) + + if ( ignoreOwnerDamage == null ) + return false + + return ignoreOwnerDamage == 1 +} + +bool function WeaponIsSmartPistolVariant( entity weapon ) +{ + var isSP = weapon.GetWeaponInfoFileKeyField( "is_smart_pistol" ) + + //printt( isSP ) + + if ( isSP == null ) + return false + + return ( isSP == 1 ) +} + +// NOTE: we should stop using this +function TrapDestroyOnRoundEnd( entity player, entity trapEnt ) +{ + trapEnt.EndSignal( "OnDestroy" ) + + svGlobal.levelEnt.WaitSignal( "ClearedPlayers" ) + + if ( IsValid( trapEnt ) ) + trapEnt.Destroy() +} + +function AddPlayerScoreForTrapDestruction( entity player, entity trapEnt ) +{ + // don't get score for killing your own trap + if ( "originalOwner" in trapEnt.s && trapEnt.s.originalOwner == player ) + return + + string trapClass = trapEnt.ProjectileGetWeaponClassName() + if ( trapClass == "" ) + return + + string scoreEvent + if ( trapClass == "mp_weapon_satchel" ) + scoreEvent = "Destroyed_Satchel" + else if ( trapClass == "mp_weapon_proximity_mine" ) + scoreEvent = "Destored_Proximity_Mine" + + if ( scoreEvent == "" ) + return + + AddPlayerScore( player, scoreEvent, trapEnt ) +} + +table function GetBulletPassThroughTargets( entity attacker, WeaponBulletHitParams hitParams ) +{ + //HACK requires code later + table passThroughInfo = { + endPos = null + targetArray = [] + } + + TraceResults result + array ignoreEnts = [ attacker, hitParams.hitEnt ] + + while ( true ) + { + vector vec = ( hitParams.hitPos - hitParams.startPos ) * 1000 + ArrayRemoveInvalid( ignoreEnts ) + result = TraceLine( hitParams.startPos, vec, ignoreEnts, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + if ( result.hitEnt == svGlobal.worldspawn ) + break + + ignoreEnts.append( result.hitEnt ) + + if ( IsValidPassThroughTarget( result.hitEnt, attacker ) ) + passThroughInfo.targetArray.append( result.hitEnt ) + } + passThroughInfo.endPos = result.endPos + + return passThroughInfo +} +#endif // SERVER + +bool function WeaponCanCrit( entity weapon ) +{ + // player sometimes has no weapon during titan exit, mantle, etc... + if ( !weapon ) + return false + + return weapon.GetWeaponSettingBool( eWeaponVar.critical_hit ) +} + + +#if SERVER +bool function IsValidPassThroughTarget( entity target, entity attacker ) +{ + //Tied to PassThroughHack function remove when supported by code. + if ( target == svGlobal.worldspawn ) + return false + + if ( !IsValid( target ) ) + return false + + if ( target.GetTeam() == attacker.GetTeam() ) + return false + + if ( target.GetTeam() != TEAM_IMC && target.GetTeam() != TEAM_MILITIA ) + return false + + return true +} + +function PassThroughDamage( entity weapon, targetArray ) +{ + //Tied to PassThroughHack function remove when supported by code. + + int damageSourceID = weapon.GetDamageSourceID() + entity owner = weapon.GetWeaponOwner() + + foreach ( ent in targetArray ) + { + expect entity( ent ) + + float distanceToTarget = Distance( weapon.GetOrigin(), ent.GetOrigin() ) + float damageToDeal = CalcWeaponDamage( owner, ent, weapon, distanceToTarget, 0 ) + + ent.TakeDamage( damageToDeal, owner, weapon.GetWeaponOwner(), { damageSourceId = damageSourceID } ) + } +} +#endif // SERVER + +vector function GetVectorFromPositionToCrosshair( entity player, vector startPos ) +{ + Assert( IsValid( player ) ) + + // See where we're looking + vector traceStart = player.EyePosition() + vector traceEnd = traceStart + ( player.GetViewVector() * 20000 ) + local ignoreEnts = [ player ] + TraceResults traceResult = TraceLine( traceStart, traceEnd, ignoreEnts, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + // Return vec from startPos to where we are looking + vector vec = traceResult.endPos - startPos + vec = Normalize( vec ) + return vec +} + +/* +function InitMissileForRandomDriftBasic( missile, startPos, startDir ) +{ + missile.s.RandomFloatRange <- RandomFloat( 1.0 ) + missile.s.startPos <- startPos + missile.s.startDir <- startDir +} +*/ + +function InitMissileForRandomDriftForVortexHigh( entity missile, vector startPos, vector startDir ) +{ + missile.InitMissileForRandomDrift( startPos, startDir, 8, 2.5, 0, 0, 100, 100 ) +} + +function InitMissileForRandomDriftForVortexLow( entity missile, vector startPos, vector startDir ) +{ + missile.InitMissileForRandomDrift( startPos, startDir, 0.3, 0.085, 0, 0, 0.5, 0.5 ) +} + +/* +function InitMissileForRandomDrift( missile, startPos, startDir ) +{ + InitMissileForRandomDriftBasic( missile, startPos, startDir ) + + missile.s.drift_windiness <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_drift_windiness" ) + missile.s.drift_intensity <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_drift_intensity" ) + + missile.s.straight_time_min <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_straight_time_min" ) + missile.s.straight_time_max <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_straight_time_max" ) + + missile.s.straight_radius_min <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_straight_radius_min" ) + if ( missile.s.straight_radius_min < 1 ) + missile.s.straight_radius_min = 1 + missile.s.straight_radius_max <- missile.ProjectileGetWeaponInfoFileKeyField( "projectile_straight_radius_max" ) + if ( missile.s.straight_radius_max < 1 ) + missile.s.straight_radius_max = 1 +} + +function SmoothRandom( x ) +{ + return 0.25 * (sin(x) + sin(x * 0.762) + sin(x * 0.363) + sin(x * 0.084)) +} + +function MissileRandomDrift( timeElapsed, timeStep, windiness, intensity ) +{ + // This function makes the missile go in a random direction. + // Windiness is how frequently the missile changes direction. + // Intensity is how strongly the missile steers in the direction it has chosen. + + local sampleTime = timeElapsed - timeStep * 0.5 + + intensity *= timeStep + + local offset = self.s.RandomFloatRange * 1000 + + local offsetx = intensity * SmoothRandom( offset + sampleTime * windiness ) + local offsety = intensity * SmoothRandom( offset * 2 + 100 + sampleTime * windiness ) + + local right = self.GetRightVector() + local up = self.GetUpVector() + + //DebugDrawLine( self.GetOrigin(), self.GetOrigin() + right * 100, 255,255,255, true, 0 ) + //DebugDrawLine( self.GetOrigin(), self.GetOrigin() + up * 100, 255,128,255, true, 0 ) + + local dir = self.GetVelocity() + local speed = Length( dir ) + dir = Normalize( dir ) + dir += right * offsetx + dir += up * offsety + dir = Normalize( dir ) + dir *= speed + + return dir +} + +// designed to be called every frame (GetProjectileVelocity callback) on projectiles that are flying through the air +function ApplyMissileControlledDrift( missile, timeElapsed, timeStep ) +{ + // If we have a target, don't do anything fancy; just let code do the homing behavior + if ( missile.GetMissileTarget() ) + return missile.GetVelocity() + + local s = missile.s + return MissileControlledDrift( timeElapsed, timeStep, s.drift_windiness, s.drift_intensity, s.straight_time_min, s.straight_time_max, s.straight_radius_min, s.straight_radius_max ) +} + +function MissileControlledDrift( timeElapsed, timeStep, windiness, intensity, pathTimeMin, pathTimeMax, pathRadiusMin, pathRadiusMax ) +{ + // Start with random drift. + local vel = MissileRandomDrift( timeElapsed, timeStep, windiness, intensity ) + + // Straighten our velocity back along our original path if we're below pathTimeMax. + // Path time is how long it tries to stay on a straight path. + // Path radius is how far it can get from its straight path. + if ( timeElapsed < pathTimeMax ) + { + local org = self.GetOrigin() + local alongPathLen = self.s.startDir.Dot( org - self.s.startPos ) + local alongPathPos = self.s.startPos + self.s.startDir * alongPathLen + local offPathOffset = org - alongPathPos + local pathDist = Length( offPathOffset ) + + local speed = Length( vel ) + + local lerp = 1 + if ( timeElapsed > pathTimeMin ) + lerp = 1.0 - (timeElapsed - pathTimeMin) / (pathTimeMax - pathTimeMin) + + local pathRadius = pathRadiusMax + (pathRadiusMin - pathRadiusMax) * lerp + + // This circle shows the radius the missile is allowed to be in. + //if ( IsServer() ) + // DebugDrawCircle( alongPathPos, VectorToAngles( AnglesToUp( VectorToAngles( self.s.startDir ) ) ), pathRadius, 255,255,255, true, 0.0 ) + + local backToPathVel = offPathOffset * -1 + // Cap backToPathVel at speed + if ( pathDist > pathRadius ) + backToPathVel *= speed / pathDist + else + backToPathVel *= speed / pathRadius + + if ( pathDist < pathRadius ) + { + backToPathVel += self.s.startDir * (speed * (1.0 - pathDist / pathRadius)) + } + + //DebugDrawLine( org, org + vel * 0.1, 255,255,255, true, 0 ) + //DebugDrawLine( org, org + backToPathVel * intensity * lerp * 0.1, 128,255,128, true, 0 ) + + vel += backToPathVel * (intensity * timeStep) + vel = Normalize( vel ) + vel *= speed + } + + return vel +} +*/ + +#if SERVER +function ClusterRocket_Detonate( entity rocket, vector normal ) +{ + entity owner = rocket.GetOwner() + if ( !IsValid( owner ) ) + return + + int count + float duration + float range + + array mods = rocket.ProjectileGetMods() + if ( mods.contains( "pas_northstar_cluster" ) ) + { + count = CLUSTER_ROCKET_BURST_COUNT_BURN + duration = PAS_NORTHSTAR_CLUSTER_ROCKET_DURATION + range = CLUSTER_ROCKET_BURST_RANGE * 1.5 + } + else + { + count = CLUSTER_ROCKET_BURST_COUNT + duration = CLUSTER_ROCKET_DURATION + range = CLUSTER_ROCKET_BURST_RANGE + } + + if ( mods.contains( "fd_twin_cluster" ) ) + { + count = int( count * 0.7 ) + duration *= 0.7 + } + PopcornInfo popcornInfo + + popcornInfo.weaponName = "mp_titanweapon_dumbfire_rockets" + popcornInfo.weaponMods = mods + popcornInfo.damageSourceId = eDamageSourceId.mp_titanweapon_dumbfire_rockets + popcornInfo.count = count + popcornInfo.delay = CLUSTER_ROCKET_BURST_DELAY + popcornInfo.offset = CLUSTER_ROCKET_BURST_OFFSET + popcornInfo.range = range + popcornInfo.normal = normal + popcornInfo.duration = duration + popcornInfo.groupSize = CLUSTER_ROCKET_BURST_GROUP_SIZE + popcornInfo.hasBase = true + + thread StartClusterExplosions( rocket, owner, popcornInfo, CLUSTER_ROCKET_FX_TABLE ) +} + + +function StartClusterExplosions( entity projectile, entity owner, PopcornInfo popcornInfo, customFxTable = null ) +{ + Assert( IsValid( owner ) ) + owner.EndSignal( "OnDestroy" ) + + string weaponName = popcornInfo.weaponName + float innerRadius + float outerRadius + int explosionDamage + int explosionDamageHeavyArmor + + innerRadius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosion_inner_radius ) + outerRadius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosionradius ) + if ( owner.IsPlayer() ) + { + explosionDamage = projectile.GetProjectileWeaponSettingInt( eWeaponVar.explosion_damage ) + explosionDamageHeavyArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.explosion_damage_heavy_armor ) + } + else + { + explosionDamage = projectile.GetProjectileWeaponSettingInt( eWeaponVar.npc_explosion_damage ) + explosionDamageHeavyArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.npc_explosion_damage_heavy_armor ) + } + + local explosionDelay = projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_explosion_delay" ) + + if ( owner.IsPlayer() ) + owner.EndSignal( "OnDestroy" ) + + vector origin = projectile.GetOrigin() + + vector rotateFX = Vector( 90,0,0 ) + entity placementHelper = CreateScriptMover() + placementHelper.SetOrigin( origin ) + placementHelper.SetAngles( VectorToAngles( popcornInfo.normal ) ) + SetTeam( placementHelper, owner.GetTeam() ) + + array players = GetPlayerArray() + foreach ( player in players ) + { + Remote_CallFunction_NonReplay( player, "SCB_AddGrenadeIndicatorForEntity", owner.GetTeam(), owner.GetEncodedEHandle(), placementHelper.GetEncodedEHandle(), outerRadius ) + } + + int particleSystemIndex = GetParticleSystemIndex( CLUSTER_BASE_FX ) + int attachId = placementHelper.LookupAttachment( "REF" ) + entity fx + + if ( popcornInfo.hasBase ) + { + fx = StartParticleEffectOnEntity_ReturnEntity( placementHelper, particleSystemIndex, FX_PATTACH_POINT_FOLLOW, attachId ) + EmitSoundOnEntity( placementHelper, "Explo_ThermiteGrenade_Impact_3P" ) // TODO: wants a custom sound + } + + OnThreadEnd( + function() : ( fx, placementHelper ) + { + if ( IsValid( fx ) ) + EffectStop( fx ) + placementHelper.Destroy() + } + ) + + if ( explosionDelay ) + wait explosionDelay + + waitthread ClusterRocketBursts( origin, explosionDamage, explosionDamageHeavyArmor, innerRadius, outerRadius, owner, popcornInfo, customFxTable ) + + if ( IsValid( projectile ) ) + projectile.Destroy() +} + + +//------------------------------------------------------------ +// ClusterRocketBurst() - does a "popcorn airburst" explosion effect over time around the origin. Total distance is based on popRangeBase +// - returns the entity in case you want to parent it +//------------------------------------------------------------ +function ClusterRocketBursts( vector origin, int damage, int damageHeavyArmor, float innerRadius, float outerRadius, entity owner, PopcornInfo popcornInfo, customFxTable = null ) +{ + owner.EndSignal( "OnDestroy" ) + + // this ent remembers the weapon mods + entity clusterExplosionEnt = CreateEntity( "info_target" ) + DispatchSpawn( clusterExplosionEnt ) + + if ( popcornInfo.weaponMods.len() > 0 ) + clusterExplosionEnt.s.weaponMods <- popcornInfo.weaponMods + + clusterExplosionEnt.SetOwner( owner ) + clusterExplosionEnt.SetOrigin( origin ) + + AI_CreateDangerousArea_Static( clusterExplosionEnt, null, outerRadius, TEAM_INVALID, true, true, origin ) + + OnThreadEnd( + function() : ( clusterExplosionEnt ) + { + clusterExplosionEnt.Destroy() + } + ) + + // No Damage - Only Force + // Push players + // Test LOS before pushing + int flags = 11 + // create a blast that knocks pilots out of the way + CreatePhysExplosion( origin, outerRadius, PHYS_EXPLOSION_LARGE, flags ) + + int count = popcornInfo.groupSize + for ( int index = 0; index < count; index++ ) + { + thread ClusterRocketBurst( clusterExplosionEnt, origin, damage, damageHeavyArmor, innerRadius, outerRadius, owner, popcornInfo, customFxTable ) + WaitFrame() + } + + wait CLUSTER_ROCKET_DURATION +} + +function ClusterRocketBurst( entity clusterExplosionEnt, vector origin, damage, damageHeavyArmor, innerRadius, outerRadius, entity owner, PopcornInfo popcornInfo, customFxTable = null ) +{ + clusterExplosionEnt.EndSignal( "OnDestroy" ) + Assert( IsValid( owner ), "ClusterRocketBurst had invalid owner" ) + + // first explosion always happens where you fired + //int eDamageSource = popcornInfo.damageSourceId + int numBursts = popcornInfo.count + float popRangeBase = popcornInfo.range + float popDelayBase = popcornInfo.delay + float popDelayRandRange = popcornInfo.offset + float duration = popcornInfo.duration + int groupSize = popcornInfo.groupSize + + int counter = 0 + vector randVec + float randRangeMod + float popRange + vector popVec + vector popOri = origin + float popDelay + float colTrace + + float burstDelay = duration / ( numBursts / groupSize ) + + vector clusterBurstOrigin = origin + (popcornInfo.normal * 8.0) + entity clusterBurstEnt = CreateClusterBurst( clusterBurstOrigin ) + + OnThreadEnd( + function() : ( clusterBurstEnt ) + { + if ( IsValid( clusterBurstEnt ) ) + { + foreach ( fx in clusterBurstEnt.e.fxArray ) + { + if ( IsValid( fx ) ) + fx.Destroy() + } + clusterBurstEnt.Destroy() + } + } + ) + + while ( IsValid( clusterBurstEnt ) && counter <= numBursts / popcornInfo.groupSize ) + { + randVec = RandomVecInDome( popcornInfo.normal ) + randRangeMod = RandomFloat( 1.0 ) + popRange = popRangeBase * randRangeMod + popVec = randVec * popRange + popOri = origin + popVec + popDelay = popDelayBase + RandomFloatRange( -popDelayRandRange, popDelayRandRange ) + + colTrace = TraceLineSimple( origin, popOri, null ) + if ( colTrace < 1 ) + { + popVec = popVec * colTrace + popOri = origin + popVec + } + + clusterBurstEnt.SetOrigin( clusterBurstOrigin ) + + vector velocity = GetVelocityForDestOverTime( clusterBurstEnt.GetOrigin(), popOri, burstDelay - popDelay ) + clusterBurstEnt.SetVelocity( velocity ) + + clusterBurstOrigin = popOri + + counter++ + + wait burstDelay - popDelay + + Explosion( + clusterBurstOrigin, + owner, + clusterExplosionEnt, + damage, + damageHeavyArmor, + innerRadius, + outerRadius, + SF_ENVEXPLOSION_NOSOUND_FOR_ALLIES, + clusterBurstOrigin, + damage, + damageTypes.explosive, + popcornInfo.damageSourceId, + customFxTable ) + } +} + + +entity function CreateClusterBurst( vector origin ) +{ + entity prop_physics = CreateEntity( "prop_physics" ) + prop_physics.SetValueForModelKey( $"models/weapons/bullets/projectile_rocket.mdl" ) + prop_physics.kv.spawnflags = 4 // 4 = SF_PHYSPROP_DEBRIS + prop_physics.kv.fadedist = 2000 + prop_physics.kv.renderamt = 255 + prop_physics.kv.rendercolor = "255 255 255" + prop_physics.kv.CollisionGroup = TRACE_COLLISION_GROUP_DEBRIS + + prop_physics.kv.minhealthdmg = 9999 + prop_physics.kv.nodamageforces = 1 + prop_physics.kv.inertiaScale = 1.0 + + prop_physics.SetOrigin( origin ) + DispatchSpawn( prop_physics ) + prop_physics.SetModel( $"models/weapons/grenades/m20_f_grenade.mdl" ) + + entity fx = PlayFXOnEntity( $"P_wpn_dumbfire_burst_trail", prop_physics ) + prop_physics.e.fxArray.append( fx ) + + return prop_physics +} +#endif // SERVER + +vector function GetVelocityForDestOverTime( vector startPoint, vector endPoint, float duration ) +{ + const GRAVITY = 750 + + float Vox = (endPoint.x - startPoint.x) / duration + float Voy = (endPoint.y - startPoint.y) / duration + float Voz = (endPoint.z + 0.5 * GRAVITY * duration * duration - startPoint.z) / duration + + return Vector( Vox, Voy, Voz ) +} + +vector function GetPlayerVelocityForDestOverTime( vector startPoint, vector endPoint, float duration ) +{ + // Same as above but accounts for player gravity setting not being 1.0 + + float gravityScale = expect float( GetPlayerSettingsFieldForClassName( DEFAULT_PILOT_SETTINGS, "gravityscale" ) ) + float GRAVITY = 750 * gravityScale // adjusted for new gravity scale + + float Vox = (endPoint.x - startPoint.x) / duration + float Voy = (endPoint.y - startPoint.y) / duration + float Voz = (endPoint.z + 0.5 * GRAVITY * duration * duration - startPoint.z) / duration + + return Vector( Vox, Voy, Voz ) +} + +bool function HasLockedTarget( weapon ) +{ + if ( weapon.SmartAmmo_IsEnabled() ) + { + local targets = weapon.SmartAmmo_GetTargets() + if ( targets.len() > 0 ) + { + foreach ( target in targets ) + { + if ( target.fraction == 1 ) + return true + } + } + } + return false +} + +function CanWeaponShootWhileRunning( entity weapon ) +{ + if ( "primary_fire_does_not_block_sprint" in weapon.s ) + return weapon.s.primary_fire_does_not_block_sprint + + if ( weapon.GetWeaponInfoFileKeyField( "primary_fire_does_not_block_sprint" ) == 1 ) + { + weapon.s.primary_fire_does_not_block_sprint <- true + return true + } + + weapon.s.primary_fire_does_not_block_sprint <- false + return false +} + +#if CLIENT +function ServerCallback_GuidedMissileDestroyed() +{ + entity player = GetLocalViewPlayer() + + // guided missiles has not been updated to work with replays. added this if statement defensively just in case. - Roger + if ( !( "missileInFlight" in player.s ) ) + return + + player.s.missileInFlight = false +} + +function ServerCallback_AirburstIconUpdate( toggle ) +{ + entity player = GetLocalViewPlayer() + entity cockpit = player.GetCockpit() + if ( cockpit ) + { + entity mainVGUI = cockpit.e.mainVGUI + if ( mainVGUI ) + { + if ( toggle ) + cockpit.s.offhandHud[OFFHAND_RIGHT].icon.SetImage( $"vgui/HUD/dpad_airburst_activate" ) + else + cockpit.s.offhandHud[OFFHAND_RIGHT].icon.SetImage( $"vgui/HUD/dpad_airburst" ) + } + } +} + +bool function IsOwnerViewPlayerFullyADSed( entity weapon ) +{ + entity owner = weapon.GetOwner() + if ( !IsValid( owner ) ) + return false + + if( !owner.IsPlayer() ) + return false + + if ( owner != GetLocalViewPlayer() ) + return false + + float zoomFrac = owner.GetZoomFrac() + if ( zoomFrac < 1.0 ) + return false + + return true + +} +#endif // CLIENT + +array function FireExpandContractMissiles( entity weapon, WeaponPrimaryAttackParams attackParams, vector attackPos, vector attackDir, int damageType, int explosionDamageType, shouldPredict, int rocketsPerShot, missileSpeed, launchOutAng, launchOutTime, launchInAng, launchInTime, launchInLerpTime, launchStraightLerpTime, applyRandSpread, int burstFireCountOverride = -1, debugDrawPath = false ) +{ + local missileVecs = GetExpandContractRocketTrajectories( weapon, attackParams.burstIndex, attackPos, attackDir, rocketsPerShot, launchOutAng, launchInAng, burstFireCountOverride ) + entity owner = weapon.GetWeaponOwner() + array firedMissiles + + vector missileEndPos = owner.EyePosition() + ( attackDir * 5000 ) + + for ( int i = 0; i < rocketsPerShot; i++ ) + { + entity missile = weapon.FireWeaponMissile( attackPos, attackDir, missileSpeed, damageType, explosionDamageType, false, shouldPredict ) + + if ( missile ) + { + /* + missile.s.flightData <- { + launchOutVec = missileVecs[i].outward, + launchOutTime = launchOutTime, + launchInLerpTime = launchInLerpTime, + launchInVec = missileVecs[i].inward, + launchInTime = launchInTime, + launchStraightLerpTime = launchStraightLerpTime, + endPos = missileEndPos, + applyRandSpread = applyRandSpread + } + */ + + missile.InitMissileExpandContract( missileVecs[i].outward, missileVecs[i].inward, launchOutTime, launchInLerpTime, launchInTime, launchStraightLerpTime, missileEndPos, applyRandSpread ) + + if ( IsServer() && debugDrawPath ) + thread DebugDrawMissilePath( missile ) + + //InitMissileForRandomDrift( missile, attackPos, attackDir ) + missile.InitMissileForRandomDriftFromWeaponSettings( attackPos, attackDir ) + + firedMissiles.append( missile ) + } + } + + return firedMissiles +} + +array function FireExpandContractMissiles_S2S( entity weapon, WeaponPrimaryAttackParams attackParams, vector attackPos, vector attackDir, shouldPredict, int rocketsPerShot, missileSpeed, launchOutAng, launchOutTime, launchInAng, launchInTime, launchInLerpTime, launchStraightLerpTime, applyRandSpread, int burstFireCountOverride = -1, debugDrawPath = false ) +{ + local missileVecs = GetExpandContractRocketTrajectories( weapon, attackParams.burstIndex, attackPos, attackDir, rocketsPerShot, launchOutAng, launchInAng, burstFireCountOverride ) + entity owner = weapon.GetWeaponOwner() + array firedMissiles + + vector missileEndPos = attackPos + ( attackDir * 5000 ) + + for ( int i = 0; i < rocketsPerShot; i++ ) + { + entity missile = weapon.FireWeaponMissile( attackPos, attackDir, missileSpeed, DF_GIB | DF_IMPACT, damageTypes.explosive, false, shouldPredict ) + missile.SetOrigin( attackPos )//HACK why do I have to do this? + if ( missile ) + { + /* + missile.s.flightData <- { + launchOutVec = missileVecs[i].outward, + launchOutTime = launchOutTime, + launchInLerpTime = launchInLerpTime, + launchInVec = missileVecs[i].inward, + launchInTime = launchInTime, + launchStraightLerpTime = launchStraightLerpTime, + endPos = missileEndPos, + applyRandSpread = applyRandSpread + } + */ + + missile.InitMissileExpandContract( missileVecs[i].outward, missileVecs[i].inward, launchOutTime, launchInLerpTime, launchInTime, launchStraightLerpTime, missileEndPos, applyRandSpread ) + + if ( IsServer() && debugDrawPath ) + thread DebugDrawMissilePath( missile ) + + //InitMissileForRandomDrift( missile, attackPos, attackDir ) + missile.InitMissileForRandomDriftFromWeaponSettings( attackPos, attackDir ) + + firedMissiles.append( missile ) + } + } + + return firedMissiles +} + +function GetExpandContractRocketTrajectories( entity weapon, int burstIndex, vector attackPos, vector attackDir, int rocketsPerShot, launchOutAng, launchInAng, int burstFireCount = -1 ) +{ + bool DEBUG_DRAW_MATH = false + + if ( burstFireCount == -1 ) + burstFireCount = weapon.GetWeaponBurstFireCount() + + local additionalRotation = ( ( 360.0 / rocketsPerShot ) / burstFireCount ) * burstIndex + //printt( "burstIndex:", burstIndex ) + //printt( "rocketsPerShot:", rocketsPerShot ) + //printt( "burstFireCount:", burstFireCount ) + + vector ang = VectorToAngles( attackDir ) + vector forward = AnglesToForward( ang ) + vector right = AnglesToRight( ang ) + vector up = AnglesToUp( ang ) + + if ( DEBUG_DRAW_MATH ) + DebugDrawLine( attackPos, attackPos + ( forward * 1000 ), 255, 0, 0, true, 30.0 ) + + // Create points on circle + float offsetAng = 360.0 / rocketsPerShot + for ( int i = 0; i < rocketsPerShot; i++ ) + { + local a = offsetAng * i + additionalRotation + vector vec = Vector( 0, 0, 0 ) + vec += up * deg_sin( a ) + vec += right * deg_cos( a ) + + if ( DEBUG_DRAW_MATH ) + DebugDrawLine( attackPos, attackPos + ( vec * 50 ), 10, 10, 10, true, 30.0 ) + } + + // Create missile points + vector x = right * deg_sin( launchOutAng ) + vector y = up * deg_sin( launchOutAng ) + vector z = forward * deg_cos( launchOutAng ) + vector rx = right * deg_sin( launchInAng ) + vector ry = up * deg_sin( launchInAng ) + vector rz = forward * deg_cos( launchInAng ) + local missilePoints = [] + for ( int i = 0; i < rocketsPerShot; i++ ) + { + local points = {} + + // Outward vec + local a = offsetAng * i + additionalRotation + float s = deg_sin( a ) + float c = deg_cos( a ) + vector vecOut = z + x * c + y * s + vecOut = Normalize( vecOut ) + points.outward <- vecOut + + // Inward vec + vector vecIn = rz + rx * c + ry * s + points.inward <- vecIn + + // Add to array + missilePoints.append( points ) + + if ( DEBUG_DRAW_MATH ) + { + DebugDrawLine( attackPos, attackPos + ( vecOut * 50 ), 255, 255, 0, true, 30.0 ) + DebugDrawLine( attackPos + vecOut * 50, attackPos + vecOut * 50 + ( vecIn * 50 ), 255, 0, 255, true, 30.0 ) + } + } + + return missilePoints +} + +function DebugDrawMissilePath( entity missile ) +{ + EndSignal( missile, "OnDestroy" ) + vector lastPos = missile.GetOrigin() + while ( true ) + { + WaitFrame() + if ( !IsValid( missile ) ) + return + DebugDrawLine( lastPos, missile.GetOrigin(), 0, 255, 0, true, 20.0 ) + lastPos = missile.GetOrigin() + } +} + + +function RegenerateOffhandAmmoOverTime( entity weapon, float rechargeTime, int maxAmmo, int offhandIndex ) +{ + weapon.Signal( "RegenAmmo" ) + weapon.EndSignal( "RegenAmmo" ) + weapon.EndSignal( "OnDestroy" ) + + #if CLIENT + entity weaponOwner = weapon.GetWeaponOwner() + if ( IsValid( weaponOwner ) && weaponOwner.IsPlayer() ) + { + entity cockpit = weaponOwner.GetCockpit() + if ( IsValid( cockpit ) ) + { + cockpit.s.offhandHud[offhandIndex].bar.SetBarProgressSource( ProgressSource.PROGRESS_SOURCE_SCRIPTED ) + cockpit.s.offhandHud[offhandIndex].bar.SetBarProgressRemap( 0.0, 1.0, 0.0, 1.0 ) + cockpit.s.offhandHud[offhandIndex].bar.SetBarProgressAndRate( 1.0 / maxAmmo , 1 / ( rechargeTime * maxAmmo ) ) + } + } + #endif + + if ( !( "totalChargeTime" in weapon.s ) ) + weapon.s.totalChargeTime <- rechargeTime + + if ( !( "nextChargeTime" in weapon.s ) ) + weapon.s.nextChargeTime <- null + + for ( ;; ) + { + weapon.s.nextChargeTime = rechargeTime + Time() + + wait rechargeTime + + if ( IsServer() ) + { + int max = maxAmmo + int weaponMax = weapon.GetWeaponPrimaryClipCountMax() + if ( weaponMax < max ) + max = weaponMax + + int ammo = weapon.GetWeaponPrimaryClipCount() + if ( ammo < max ) + weapon.SetWeaponPrimaryClipCount( ammo + 1 ) + } + } +} + +bool function IsPilotShotgunWeapon( string weaponName ) +{ + return GetWeaponInfoFileKeyField_Global( weaponName, "weaponSubClass" ) == "shotgun" +} + +array function GetWeaponBurnMods( string weaponClassName ) +{ + array burnMods = [] + array mods = GetWeaponMods_Global( weaponClassName ) + string prefix = "burn_mod" + foreach ( mod in mods ) + { + if ( mod.find( prefix ) == 0 ) + burnMods.append( mod ) + } + + return burnMods +} + +int function TEMP_GetDamageFlagsFromProjectile( entity projectile ) +{ + var damageFlagsString = projectile.ProjectileGetWeaponInfoFileKeyField( "damage_flags" ) + if ( damageFlagsString == null ) + return 0 + expect string( damageFlagsString ) + + return TEMP_GetDamageFlagsFromString( damageFlagsString ) +} + +int function TEMP_GetDamageFlagsFromString( string damageFlagsString ) +{ + int damageFlags = 0 + + array damageFlagTokens = split( damageFlagsString, "|" ) + foreach ( token in damageFlagTokens ) + { + damageFlags = damageFlags | getconsttable()[strip(token)] + } + + return damageFlags +} + +#if SERVER +function PROTO_InitTrackedProjectile( entity projectile ) +{ + // HACK: accessing ProjectileGetWeaponInfoFileKeyField or ProjectileGetWeaponClassName during CodeCallback_OnSpawned causes a code assert + projectile.EndSignal( "OnDestroy" ) + WaitFrame() + + entity owner = projectile.GetOwner() + + if ( !IsValid( owner ) || !owner.IsPlayer() ) + return + + int maxDeployed = projectile.GetProjectileWeaponSettingInt( eWeaponVar.projectile_max_deployed ) + if ( maxDeployed != 0 ) + { + AddToScriptManagedEntArray( owner.s.activeTrapArrayId, projectile ) + + array traps = GetScriptManagedEntArray( owner.s.activeTrapArrayId ) + array sameTypeTrapEnts + foreach ( ent in traps ) + { + if ( ent.ProjectileGetWeaponClassName() != projectile.ProjectileGetWeaponClassName() ) + continue + + sameTypeTrapEnts.append( ent ) + } + + int numToDestroy = sameTypeTrapEnts.len() - maxDeployed + if ( numToDestroy > 0 ) + { + sameTypeTrapEnts.sort( CompareCreation ) + foreach ( ent in sameTypeTrapEnts ) + { + ent.Destroy() + numToDestroy-- + + if ( !numToDestroy ) + break + } + } + } +} + + +function PROTO_CleanupTrackedProjectiles( entity player ) +{ + array traps = GetScriptManagedEntArray( player.s.activeTrapArrayId ) + foreach ( ent in traps ) + { + ent.Destroy() + } +} + +int function CompareCreation( entity a, entity b ) +{ + if ( a.GetProjectileCreationTime() > b.GetProjectileCreationTime() ) + return 1 + + return -1 +} + +int function CompareCreationReverse( entity a, entity b ) +{ + if ( a.GetProjectileCreationTime() > b.GetProjectileCreationTime() ) + return 1 + + return -1 +} + +void function PROTO_TrackedProjectile_OnPlayerRespawned( entity player ) +{ + thread PROTO_TrackedProjectile_OnPlayerRespawned_Internal( player ) +} + +void function PROTO_TrackedProjectile_OnPlayerRespawned_Internal( entity player ) +{ + player.EndSignal( "OnDeath" ) + + if ( player.s.inGracePeriod ) + player.WaitSignal( "GracePeriodDone" ) + + entity ordnance = player.GetOffhandWeapon( OFFHAND_ORDNANCE ) + + array traps = GetScriptManagedEntArray( player.s.activeTrapArrayId ) + foreach ( ent in traps ) + { + if ( ordnance && ent.ProjectileGetWeaponClassName() == ordnance.GetWeaponClassName() ) + continue + + ent.Destroy() + } +} + +function PROTO_PlayTrapLightEffect( entity ent, string tag, int team ) +{ + asset ownerFx = ent.ProjectileGetWeaponInfoFileKeyFieldAsset( "trap_warning_owner_fx" ) + if ( ownerFx != $"" ) + { + entity ownerFxEnt = CreateServerEffect_Owner( ownerFx, ent.GetOwner() ) + SetServerEffectControlPoint( ownerFxEnt, 0, FRIENDLY_COLOR ) + StartServerEffectOnEntity( ownerFxEnt, ent, tag ) + } + + asset friendlyFx = ent.ProjectileGetWeaponInfoFileKeyFieldAsset( "trap_warning_friendly_fx" ) + if ( friendlyFx != $"" ) + { + entity friendlyFxEnt = CreateServerEffect_Friendly( friendlyFx, team ) + SetServerEffectControlPoint( friendlyFxEnt, 0, FRIENDLY_COLOR_FX ) + StartServerEffectOnEntity( friendlyFxEnt, ent, tag ) + } + + asset enemyFx = ent.ProjectileGetWeaponInfoFileKeyFieldAsset( "trap_warning_enemy_fx" ) + if ( enemyFx != $"" ) + { + entity enemyFxEnt = CreateServerEffect_Enemy( enemyFx, team ) + SetServerEffectControlPoint( enemyFxEnt, 0, ENEMY_COLOR_FX ) + StartServerEffectOnEntity( enemyFxEnt, ent, tag ) + } +} + +string ornull function GetCooldownBeepPrefix( weapon ) +{ + var reloadBeepPrefix = weapon.GetWeaponInfoFileKeyField( "cooldown_sound_prefix" ) + if ( reloadBeepPrefix == null ) + return null + + expect string( reloadBeepPrefix ) + + return reloadBeepPrefix +} + +void function PROTO_DelayCooldown( entity weapon ) +{ + weapon.s.nextCooldownTime = Time() + weapon.s.cooldownDelay +} + +string function GetBeepSuffixForAmmo( int currentAmmo, int maxAmmo ) +{ + float frac = float( currentAmmo ) / float( maxAmmo ) + + if ( frac >= 1.0 ) + return "_full" + + if ( frac >= 0.25 ) + return "" + + return "_low" +} + +#endif //SERVER + +bool function PROTO_CanPlayerDeployWeapon( entity player ) +{ + if ( player.IsPhaseShifted() ) + return false + + if ( player.ContextAction_IsActive() == true ) + { + if ( player.IsZiplining() ) + return true + else + return false + } + + return true +} + +#if SERVER +void function PROTO_FlakCannonMissiles( entity projectile, float speed ) +{ + projectile.EndSignal( "OnDestroy" ) + + float radius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosionradius ) + vector velocity = projectile.GetVelocity() + vector currentPos = projectile.GetOrigin() + int team = projectile.GetTeam() + + float waitTime = 0.1 + float distanceInterval = speed * waitTime + int forwardDistanceChecks = int( ceil( distanceInterval / radius ) ) + bool forceExplosion = false + while ( forceExplosion == false ) + { + currentPos = projectile.GetOrigin() + for ( int i = 0; i < forwardDistanceChecks; i++ ) + { + float frac = float( i ) / float (forwardDistanceChecks ) + if ( PROTO_FlakCannon_HasNearbyEnemies( currentPos + velocity * waitTime * frac , team, radius ) ) + { + if ( i == 0 ) + { + forceExplosion = true + break + } + else + { + projectile.SetVelocity( velocity * ( frac - 0.05 ) ) + break + } + } + } + + if ( forceExplosion == false ) + wait waitTime + } + + projectile.MissileExplode() +} + +bool function PROTO_FlakCannon_HasNearbyEnemies( vector origin, int team, float radius ) +{ + float worldSpaceCenterBuffer = 200 + + array guys = GetPlayerArrayEx( "any", TEAM_ANY, team, origin, radius + worldSpaceCenterBuffer ) + foreach ( guy in guys ) + { + if ( IsAlive( guy ) && Distance( origin, guy.GetWorldSpaceCenter() ) < radius ) + return true + } + + array ai = GetNPCArrayEx( "any", TEAM_ANY, team, origin, radius + worldSpaceCenterBuffer ) + foreach ( guy in ai ) + { + if ( IsAlive( guy ) && Distance( origin, guy.GetWorldSpaceCenter() ) < radius ) + return true + } + + return false +} +#endif // #if SERVER + +void function GiveEMPStunStatusEffects( entity ent, float duration, float fadeoutDuration = 0.5, float slowTurn = EMP_SEVERITY_SLOWTURN, float slowMove = EMP_SEVERITY_SLOWMOVE) +{ + entity target = ent.IsTitan() ? ent.GetTitanSoul() : ent + int slowEffect = StatusEffect_AddTimed( target, eStatusEffect.turn_slow, slowTurn, duration, fadeoutDuration ) + int turnEffect = StatusEffect_AddTimed( target, eStatusEffect.move_slow, slowMove, duration, fadeoutDuration ) + + #if SERVER + if ( ent.IsPlayer() ) + { + ent.p.empStatusEffectsToClearForPhaseShift.append( slowEffect ) + ent.p.empStatusEffectsToClearForPhaseShift.append( turnEffect ) + } + #endif +} + +#if DEV +string ornull function FindEnumNameForValue( table searchTable, int searchVal ) +{ + foreach( string keyname, int value in searchTable ) + { + if ( value == searchVal ) + return keyname; + } + return null +} + +void function DevPrintAllStatusEffectsOnEnt( entity ent ) +{ + printt( "Effects:", ent ) + array effects = StatusEffect_GetAll( ent ) + int length = effects.len() + int found = 0; + for ( int idx = 0; idx < length; idx++ ) + { + float severity = effects[idx]; + if ( severity <= 0.0 ) + continue + string ornull name = FindEnumNameForValue( eStatusEffect, idx ) + Assert( name ) + expect string( name ) + printt( " eStatusEffect." + name + ": " + severity ) + found++; + } + printt( found + " effects active.\n" ); +} +#endif // #if DEV + +array function GetPrimaryWeapons( entity player ) +{ + array primaryWeapons + array weapons = player.GetMainWeapons() + foreach ( weaponEnt in weapons ) + { + int weaponType = weaponEnt.GetWeaponType() + if ( weaponType == WT_SIDEARM || weaponType == WT_ANTITITAN ) + continue; + + primaryWeapons.append( weaponEnt ) + } + return primaryWeapons +} + +array function GetSidearmWeapons( entity player ) +{ + array sidearmWeapons + array weapons = player.GetMainWeapons() + foreach ( weaponEnt in weapons ) + { + if ( weaponEnt.GetWeaponType() != WT_SIDEARM ) + continue + + sidearmWeapons.append( weaponEnt ) + } + return sidearmWeapons +} + +array function GetATWeapons( entity player ) +{ + array atWeapons + array weapons = player.GetMainWeapons() + foreach ( weaponEnt in weapons ) + { + if ( weaponEnt.GetWeaponType() != WT_ANTITITAN ) + continue + + atWeapons.append( weaponEnt ) + } + return atWeapons +} + +entity function GetPlayerFromTitanWeapon( entity weapon ) +{ + entity titan = weapon.GetWeaponOwner() + entity player + + if ( titan == null ) + return null + + if ( !titan.IsPlayer() ) + player = titan.GetBossPlayer() + else + player = titan + + return player +} + + +const asset CHARGE_SHOT_PROJECTILE = $"models/weapons/bullets/temp_triple_threat_projectile_large.mdl" + +const asset CHARGE_EFFECT_1P = $"P_ordnance_charge_st_FP" // $"P_wpn_defender_charge_FP" +const asset CHARGE_EFFECT_3P = $"P_ordnance_charge_st" // $"P_wpn_defender_charge" +const asset CHARGE_EFFECT_DLIGHT = $"defender_charge_CH_dlight" + +const string CHARGE_SOUND_WINDUP_1P = "Weapon_ChargeRifle_WindUp_1P" +const string CHARGE_SOUND_WINDUP_3P = "Weapon_ChargeRifle_WindUp_3P" +const string CHARGE_SOUND_WINDDOWN_1P = "Weapon_ChargeRifle_WindDown_1P" +const string CHARGE_SOUND_WINDDOWN_3P = "Weapon_ChargeRifle_WindDown_3P" + +void function ChargeBall_Precache() +{ +#if SERVER + PrecacheModel( CHARGE_SHOT_PROJECTILE ) + PrecacheEffect( CHARGE_EFFECT_1P ) + PrecacheEffect( CHARGE_EFFECT_3P ) +#endif // #if SERVER +} + +void function ChargeBall_FireProjectile( entity weapon, vector position, vector direction, bool shouldPredict ) +{ + weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 ) + + entity owner = weapon.GetWeaponOwner() + const float MISSILE_SPEED = 1200.0 + const int CONTACT_DAMAGE_TYPES = (damageTypes.projectileImpact | DF_DOOM_FATALITY) + const int EXPLOSION_DAMAGE_TYPES = damageTypes.explosive + const bool DO_POPUP = false + + if ( shouldPredict ) + { + entity missile = weapon.FireWeaponMissile( position, direction, MISSILE_SPEED, CONTACT_DAMAGE_TYPES, EXPLOSION_DAMAGE_TYPES, DO_POPUP, shouldPredict ) + if ( missile ) + { + EmitSoundOnEntity( owner, "ShoulderRocket_Cluster_Fire_3P" ) + missile.SetModel( CHARGE_SHOT_PROJECTILE ) +#if CLIENT + const ROCKETEER_MISSILE_EXPLOSION = $"xo_exp_death" + const ROCKETEER_MISSILE_SHOULDER_FX = $"wpn_mflash_xo_rocket_shoulder_FP" + entity owner = weapon.GetWeaponOwner() + vector origin = owner.OffsetPositionFromView( Vector(0, 0, 0), Vector(25, -25, 15) ) + vector angles = owner.CameraAngles() + StartParticleEffectOnEntityWithPos( owner, GetParticleSystemIndex( ROCKETEER_MISSILE_SHOULDER_FX ), FX_PATTACH_EYES_FOLLOW, -1, origin, angles ) +#else // #if CLIENT + missile.SetProjectileImpactDamageOverride( 1440 ) + missile.kv.damageSourceId = eDamageSourceId.charge_ball +#endif // #else // #if CLIENT + } + } +} + +bool function ChargeBall_ChargeBegin( entity weapon, string tagName ) +{ +#if CLIENT + if ( InPrediction() && !IsFirstTimePredicted() ) + return true +#endif // #if CLIENT + + weapon.w.statusEffects.append( StatusEffect_AddEndless( weapon.GetWeaponOwner(), eStatusEffect.move_slow, 0.6 ) ) + weapon.w.statusEffects.append( StatusEffect_AddEndless( weapon.GetWeaponOwner(), eStatusEffect.turn_slow, 0.35 ) ) + + weapon.PlayWeaponEffect( CHARGE_EFFECT_1P, CHARGE_EFFECT_3P, tagName ) + weapon.PlayWeaponEffect( $"", CHARGE_EFFECT_DLIGHT, tagName ) + +#if SERVER + StopSoundOnEntity( weapon, CHARGE_SOUND_WINDDOWN_3P ) + entity weaponOwner = weapon.GetWeaponOwner() + if ( IsValid( weaponOwner ) ) + { + if ( weaponOwner.IsPlayer() ) + EmitSoundOnEntityExceptToPlayer( weapon, weaponOwner, CHARGE_SOUND_WINDUP_3P ) + else + EmitSoundOnEntity( weapon, CHARGE_SOUND_WINDUP_3P ) + } +#else + StopSoundOnEntity( weapon, CHARGE_SOUND_WINDDOWN_1P ) + EmitSoundOnEntity( weapon, CHARGE_SOUND_WINDUP_1P ) +#endif + + return true +} + +void function ChargeBall_ChargeEnd( entity weapon ) +{ +#if CLIENT + if ( InPrediction() && !IsFirstTimePredicted() ) + return +#endif + + if ( IsValid( weapon.GetWeaponOwner() ) ) + { + #if CLIENT + if ( InPrediction() && IsFirstTimePredicted() ) + { + #endif + + foreach ( effect in weapon.w.statusEffects ) + { + StatusEffect_Stop( weapon.GetWeaponOwner(), effect ) + } + + #if CLIENT + } + #endif + } + +#if SERVER + StopSoundOnEntity( weapon, CHARGE_SOUND_WINDUP_3P ) + entity weaponOwner = weapon.GetWeaponOwner() + if ( IsValid( weaponOwner ) ) + { + if ( weaponOwner.IsPlayer() ) + EmitSoundOnEntityExceptToPlayer( weapon, weaponOwner, CHARGE_SOUND_WINDDOWN_3P ) + else + EmitSoundOnEntity( weapon, CHARGE_SOUND_WINDDOWN_3P ) + } +#else + StopSoundOnEntity( weapon, CHARGE_SOUND_WINDUP_1P ) + EmitSoundOnEntity( weapon, CHARGE_SOUND_WINDDOWN_1P ) +#endif + + ChargeBall_StopChargeEffects( weapon ) +} + +void function ChargeBall_StopChargeEffects( entity weapon ) +{ + Assert( IsValid( weapon ) ) + // weapon.StopWeaponEffect( CHARGE_EFFECT_1P, CHARGE_EFFECT_3P ) + // weapon.StopWeaponEffect( CHARGE_EFFECT_3P, CHARGE_EFFECT_1P ) + // weapon.StopWeaponEffect( CHARGE_EFFECT_DLIGHT, CHARGE_EFFECT_DLIGHT ) + thread HACK_Deplayed_ChargeBall_StopChargeEffects( weapon ) +} + +void function HACK_Deplayed_ChargeBall_StopChargeEffects( entity weapon ) +{ + weapon.EndSignal( "OnDestroy" ) + wait 0.2 + weapon.StopWeaponEffect( CHARGE_EFFECT_1P, CHARGE_EFFECT_3P ) + weapon.StopWeaponEffect( CHARGE_EFFECT_3P, CHARGE_EFFECT_1P ) + weapon.StopWeaponEffect( CHARGE_EFFECT_DLIGHT, CHARGE_EFFECT_DLIGHT ) +} + +float function ChargeBall_GetChargeTime() +{ + return 1.05 +} + +#if SERVER +void function GivePlayerAmpedWeapon( entity player, string weaponName ) +{ + array weapons = player.GetMainWeapons() + int numWeapons = weapons.len() + if ( numWeapons == 0 ) + return + + //Figure out what weapon to take away. + //This is more complicated than it should be because of rules of what weapons can be in what slots, e.g. your anti-titan weapon can't be replaced by non anti-titan weapons + if ( HasWeapon( player, weaponName ) ) + { + //Simplest case: + //Take away the currently existing version of the weapon you already have. + player.TakeWeaponNow( weaponName ) + } + else + { + bool ampedWeaponIsAntiTitan = GetWeaponInfoFileKeyField_Global( weaponName, "weaponType" ) == "anti_titan" + if ( ampedWeaponIsAntiTitan ) + { + foreach( weapon in weapons ) + { + string currentWeaponClassName = weapon.GetWeaponClassName() + if ( GetWeaponInfoFileKeyField_Global( currentWeaponClassName, "weaponType" ) == "anti_titan" ) + { + player.TakeWeaponNow( currentWeaponClassName ) + break + } + } + + unreachable //We had no anti-titan weapon? Shouldn't ever be possible + + } + else + { + string currentActiveWeaponClassName = player.GetActiveWeapon().GetWeaponClassName() + if ( ShouldReplaceWeaponInFirstSlot( player, currentActiveWeaponClassName ) ) + { + //Current weapon is anti_titan, but amped weapon we are trying to give is not. Just replace the weapon that is in the first slot. + //Assumes that weapon in first slot is not an anti-titan weapon + //We could get even fancier and look to see if the amped weapon is a primary weapon or a sidearm and replace the slot accordingly, but + //that makes it more complicated, plus there are cases where you can have no primary weapons/no side arms etc + string firstWeaponClassName = weapons[ 0 ].GetWeaponClassName() + Assert( GetWeaponInfoFileKeyField_Global( firstWeaponClassName, "weaponType" ) != "anti_titan" ) + player.TakeWeaponNow( firstWeaponClassName ) + } + else + { + player.TakeWeaponNow( currentActiveWeaponClassName ) + } + } + } + + array burnMods = GetWeaponBurnMods( weaponName ) + entity ampedWeapon = player.GiveWeapon( weaponName, burnMods ) + ampedWeapon.SetWeaponPrimaryClipCount( ampedWeapon.GetWeaponPrimaryClipCountMax() ) //Needed for weapons that give a mod with extra clip size +} + +bool function ShouldReplaceWeaponInFirstSlot( entity player, string currentActiveWeaponClassName ) +{ + if ( GetWeaponInfoFileKeyField_Global( currentActiveWeaponClassName, "weaponType" ) == "anti_titan" ) //Active weapon is anti-titan weapon. Can't replace anti-titan weapon slot with non-anti-titan weapon + return true + + if ( currentActiveWeaponClassName == player.GetOffhandWeapon( OFFHAND_ORDNANCE ).GetWeaponClassName() ) + return true + + return false + +} + +void function GivePlayerAmpedWeaponAndSetAsActive( entity player, string weaponName ) +{ + GivePlayerAmpedWeapon( player, weaponName ) + player.SetActiveWeaponByName( weaponName ) +} + +void function ReplacePlayerOffhand( entity player, string offhandName, array mods = [] ) +{ + player.TakeOffhandWeapon( OFFHAND_SPECIAL ) + player.GiveOffhandWeapon( offhandName, OFFHAND_SPECIAL, mods ) +} + +void function ReplacePlayerOrdnance( entity player, string ordnanceName, array mods = [] ) +{ + player.TakeOffhandWeapon( OFFHAND_ORDNANCE ) + player.GiveOffhandWeapon( ordnanceName, OFFHAND_ORDNANCE, mods ) +} + +void function PAS_CooldownReduction_OnKill( entity victim, entity attacker, var damageInfo ) +{ + if ( !IsAlive( attacker ) || !IsPilot( attacker ) ) + return + + array weaponMods = GetWeaponModsFromDamageInfo( damageInfo ) + + if ( GetCurrentPlaylistVarInt( "featured_mode_tactikill", 0 ) > 0 ) + { + entity weapon = attacker.GetOffhandWeapon( OFFHAND_LEFT ) + + switch ( GetWeaponInfoFileKeyField_Global( weapon.GetWeaponClassName(), "cooldown_type" ) ) + { + case "grapple": + attacker.SetSuitGrapplePower( attacker.GetSuitGrapplePower() + 100 ) + break + + case "ammo": + case "ammo_instant": + case "ammo_deployed": + case "ammo_timed": + int maxAmmo = weapon.GetWeaponPrimaryClipCountMax() + weapon.SetWeaponPrimaryClipCountNoRegenReset( maxAmmo ) + break + + case "chargeFrac": + weapon.SetWeaponChargeFraction( 0 ) + break + + // case "mp_ability_ground_slam": + // break + + default: + Assert( false, weapon.GetWeaponClassName() + " needs to be updated to support cooldown_type setting" ) + break + } + } + else + { + if ( !PlayerHasPassive( attacker, ePassives.PAS_CDR_ON_KILL ) && !weaponMods.contains( "tactical_cdr_on_kill" ) ) + return + + entity weapon = attacker.GetOffhandWeapon( OFFHAND_LEFT ) + + switch ( GetWeaponInfoFileKeyField_Global( weapon.GetWeaponClassName(), "cooldown_type" ) ) + { + case "grapple": + attacker.SetSuitGrapplePower( attacker.GetSuitGrapplePower() + 25 ) + break + + case "ammo": + case "ammo_instant": + case "ammo_deployed": + case "ammo_timed": + int maxAmmo = weapon.GetWeaponPrimaryClipCountMax() + weapon.SetWeaponPrimaryClipCountNoRegenReset( min( maxAmmo, weapon.GetWeaponPrimaryClipCount() + ( maxAmmo / 4 ) ) ) + break + + case "chargeFrac": + weapon.SetWeaponChargeFraction( max( 0, weapon.GetWeaponChargeFraction() - 0.25 ) ) + break + + // case "mp_ability_ground_slam": + // break + + default: + Assert( false, weapon.GetWeaponClassName() + " needs to be updated to support cooldown_type setting" ) + break + } + } +} + +void function DisableWeapons( entity player, array excludeNames ) +{ + array weapons = GetPlayerWeapons( player, excludeNames ) + foreach ( weapon in weapons ) + weapon.AllowUse( false ) +} + +void function EnableWeapons( entity player, array excludeNames ) +{ + array weapons = GetPlayerWeapons( player, excludeNames ) + foreach ( weapon in weapons ) + weapon.AllowUse( true ) +} + +array function GetPlayerWeapons( entity player, array excludeNames ) +{ + array weapons = player.GetMainWeapons() + weapons.extend( player.GetOffhandWeapons() ) + + for ( int idx = weapons.len() - 1; idx > 0; idx-- ) + { + foreach ( excludeName in excludeNames ) + { + if ( weapons[idx].GetWeaponClassName() == excludeName ) + weapons.remove( idx ) + } + } + + return weapons +} + +void function WeaponAttackWave( entity ent, int projectileCount, entity inflictor, vector pos, vector dir, bool functionref( entity, int, entity, entity, vector, vector, int ) waveFunc ) +{ + ent.EndSignal( "OnDestroy" ) + + entity weapon + entity projectile + int maxCount + float step + entity owner + int damageNearValueTitanArmor + int count = 0 + array positions = [] + vector lastDownPos + bool firstTrace = true + + dir = + dir = Normalize( dir ) + vector angles = VectorToAngles( dir ) + + if ( ent.IsProjectile() ) + { + projectile = ent + string chargedPrefix = "" + if ( ent.proj.isChargedShot ) + chargedPrefix = "charge_" + + maxCount = expect int( ent.ProjectileGetWeaponInfoFileKeyField( chargedPrefix + "wave_max_count" ) ) + step = expect float( ent.ProjectileGetWeaponInfoFileKeyField( chargedPrefix + "wave_step_dist" ) ) + owner = ent.GetOwner() + damageNearValueTitanArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.damage_near_value_titanarmor ) + } + else + { + weapon = ent + maxCount = expect int( ent.GetWeaponInfoFileKeyField( "wave_max_count" ) ) + step = expect float( ent.GetWeaponInfoFileKeyField( "wave_step_dist" ) ) + owner = ent.GetWeaponOwner() + damageNearValueTitanArmor = weapon.GetWeaponSettingInt( eWeaponVar.damage_near_value_titanarmor ) + } + + owner.EndSignal( "OnDestroy" ) + + for ( int i = 0; i < maxCount; i++ ) + { + vector newPos = pos + dir * step + + vector traceStart = pos + vector traceEndUnder = newPos + vector traceEndOver = newPos + + if ( !firstTrace ) + { + traceStart = lastDownPos + <0.0, 0.0, 80.0 > + traceEndUnder = + traceEndOver = // The over height is to cover the case of a sheer surface that then continues gradually upwards (like mp_box) + } + firstTrace = false + + VortexBulletHit ornull vortexHit = VortexBulletHitCheck( owner, traceStart, traceEndOver ) + if ( vortexHit ) + { + expect VortexBulletHit( vortexHit ) + entity vortexWeapon = vortexHit.vortex.GetOwnerWeapon() + + if ( vortexWeapon && vortexWeapon.GetWeaponClassName() == "mp_titanweapon_vortex_shield" ) + VortexDrainedByImpact( vortexWeapon, weapon, projectile, null ) // drain the vortex shield + else if ( IsVortexSphere( vortexHit.vortex ) ) + VortexSphereDrainHealthForDamage( vortexHit.vortex, damageNearValueTitanArmor ) + + WaitFrame() + continue + } + + //DebugDrawLine( traceStart, traceEndUnder, 0, 255, 0, true, 25.0 ) + array ignoreArray = [] + if ( IsValid( inflictor ) && inflictor.GetOwner() != null ) + ignoreArray.append( inflictor.GetOwner() ) + + TraceResults forwardTrace = TraceLine( traceStart, traceEndUnder, ignoreArray, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS ) + if ( forwardTrace.fraction == 1.0 ) + { + //DebugDrawLine( forwardTrace.endPos, forwardTrace.endPos + <0.0, 0.0, -1000.0>, 255, 0, 0, true, 25.0 ) + TraceResults downTrace = TraceLine( forwardTrace.endPos, forwardTrace.endPos + <0.0, 0.0, -1000.0>, ignoreArray, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS ) + if ( downTrace.fraction == 1.0 ) + break + + entity movingGeo = null + if ( downTrace.hitEnt && downTrace.hitEnt.HasPusherRootParent() && !downTrace.hitEnt.IsMarkedForDeletion() ) + movingGeo = downTrace.hitEnt + + if ( !waveFunc( ent, projectileCount, inflictor, movingGeo, downTrace.endPos, angles, i ) ) + return + + lastDownPos = downTrace.endPos + pos = forwardTrace.endPos + + WaitFrame() + continue + } + else + { + if ( IsValid( forwardTrace.hitEnt ) && (StatusEffect_Get( forwardTrace.hitEnt, eStatusEffect.pass_through_amps_weapon ) > 0) && !CheckPassThroughDir( forwardTrace.hitEnt, forwardTrace.surfaceNormal, forwardTrace.endPos ) ) + break; + } + + TraceResults upwardTrace = TraceLine( traceStart, traceEndOver, ignoreArray, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS ) + //DebugDrawLine( traceStart, traceEndOver, 0, 0, 255, true, 25.0 ) + if ( upwardTrace.fraction < 1.0 ) + { + if ( IsValid( upwardTrace.hitEnt ) ) + { + if ( upwardTrace.hitEnt.IsWorld() || upwardTrace.hitEnt.IsPlayer() || upwardTrace.hitEnt.IsNPC() ) + break + } + } + else + { + TraceResults downTrace = TraceLine( upwardTrace.endPos, upwardTrace.endPos + <0.0, 0.0, -1000.0>, ignoreArray, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS ) + if ( downTrace.fraction == 1.0 ) + break + + entity movingGeo = null + if ( downTrace.hitEnt && downTrace.hitEnt.HasPusherRootParent() && !downTrace.hitEnt.IsMarkedForDeletion() ) + movingGeo = downTrace.hitEnt + + if ( !waveFunc( ent, projectileCount, inflictor, movingGeo, downTrace.endPos, angles, i ) ) + return + + lastDownPos = downTrace.endPos + pos = forwardTrace.endPos + } + + WaitFrame() + } +} + +void function AddActiveThermiteBurn( entity ent ) +{ + AddToScriptManagedEntArray( file.activeThermiteBurnsManagedEnts, ent ) +} + +array function GetActiveThermiteBurnsWithinRadius( vector origin, float dist, team = TEAM_ANY ) +{ + return GetScriptManagedEntArrayWithinCenter( file.activeThermiteBurnsManagedEnts, team, origin, dist ) +} + +void function EMP_DamagedPlayerOrNPC( entity ent, var damageInfo ) +{ + Elecriticy_DamagedPlayerOrNPC( ent, damageInfo, FX_EMP_BODY_HUMAN, FX_EMP_BODY_TITAN, EMP_SEVERITY_SLOWTURN, EMP_SEVERITY_SLOWMOVE ) +} + +void function VanguardEnergySiphon_DamagedPlayerOrNPC( entity ent, var damageInfo ) +{ + entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( IsValid( attacker ) && attacker.GetTeam() == ent.GetTeam() ) + return + + Elecriticy_DamagedPlayerOrNPC( ent, damageInfo, FX_VANGUARD_ENERGY_BODY_HUMAN, FX_VANGUARD_ENERGY_BODY_TITAN, LASER_STUN_SEVERITY_SLOWTURN, LASER_STUN_SEVERITY_SLOWMOVE ) +} + +void function Elecriticy_DamagedPlayerOrNPC( entity ent, var damageInfo, asset humanFx, asset titanFx, float slowTurn, float slowMove ) +{ + if ( !IsValid( ent ) ) + return + + if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS ) + return + + local inflictor = DamageInfo_GetInflictor( damageInfo ) + if( !IsValid( inflictor ) ) + return + + // Do electrical effect on this ent that everyone can see if they are a titan + string tag = "" + asset effect + + if ( ent.IsTitan() ) + { + tag = "exp_torso_front" + effect = titanFx + } + else if ( IsStalker( ent ) || IsSpectre( ent ) ) + { + tag = "CHESTFOCUS" + effect = humanFx + if ( !ent.ContextAction_IsActive() && IsAlive( ent ) && ent.IsInterruptable() ) + { + ent.Anim_ScriptedPlayActivityByName( "ACT_STUNNED", true, 0.1 ) + } + } + else if ( IsSuperSpectre( ent ) ) + { + tag = "CHESTFOCUS" + effect = humanFx + + if ( ent.GetParent() == null && !ent.ContextAction_IsActive() && IsAlive( ent ) && ent.IsInterruptable() ) + { + ent.Anim_ScriptedPlayActivityByName( "ACT_STUNNED", true, 0.1 ) + } + } + else if ( IsGrunt( ent ) ) + { + tag = "CHESTFOCUS" + effect = humanFx + if ( !ent.ContextAction_IsActive() && IsAlive( ent ) && ent.IsInterruptable() ) + { + ent.Anim_ScriptedPlayActivityByName( "ACT_STUNNED", true, 0.1 ) + ent.EnableNPCFlag( NPC_PAIN_IN_SCRIPTED_ANIM ) + } + } + else if ( IsPilot( ent ) ) + { + tag = "CHESTFOCUS" + effect = humanFx + } + else if ( IsAirDrone( ent ) ) + { + if ( GetDroneType( ent ) == "drone_type_marvin" ) + return + tag = "HEADSHOT" + effect = humanFx + thread NpcEmpRebootPrototype( ent, damageInfo, humanFx, titanFx ) + } + else if ( IsGunship( ent ) ) + { + tag = "ORIGIN" + effect = titanFx + thread NpcEmpRebootPrototype( ent, damageInfo, humanFx, titanFx ) + } + + ent.Signal( "ArcStunned" ) + + if ( tag != "" ) + { + local inflictor = DamageInfo_GetInflictor( damageInfo ) + Assert( !(inflictor instanceof CEnvExplosion) ) + if ( IsValid( inflictor ) ) + { + float duration = EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MAX + if ( inflictor instanceof CBaseGrenade ) + { + local entCenter = ent.GetWorldSpaceCenter() + local dist = Distance( DamageInfo_GetDamagePosition( damageInfo ), entCenter ) + local damageRadius = inflictor.GetDamageRadius() + duration = GraphCapped( dist, damageRadius * 0.5, damageRadius, EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MIN, EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MAX ) + } + thread EMP_FX( effect, ent, tag, duration ) + } + } + + if ( StatusEffect_Get( ent, eStatusEffect.destroyed_by_emp ) ) + DamageInfo_SetDamage( damageInfo, ent.GetHealth() ) + + // Don't do arc beams to entities that are on the same team... except the owner + entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( IsValid( attacker ) && attacker.GetTeam() == ent.GetTeam() && attacker != ent ) + return + + if ( ent.IsPlayer() ) + { + thread EMPGrenade_EffectsPlayer( ent, damageInfo ) + } + else if ( ent.IsTitan() ) + { + EMPGrenade_AffectsShield( ent, damageInfo ) + #if MP + GiveEMPStunStatusEffects( ent, 2.5, 1.0, slowTurn, slowMove ) + #endif + thread EMPGrenade_AffectsAccuracy( ent ) + } + else if ( ent.IsMechanical() ) + { + #if MP + GiveEMPStunStatusEffects( ent, 2.5, 1.0, slowTurn, slowMove ) + DamageInfo_ScaleDamage( damageInfo, 2.05 ) + #endif + } + else if ( ent.IsHuman() ) + { + #if MP + DamageInfo_ScaleDamage( damageInfo, 0.99 ) + #endif + } + + if ( inflictor instanceof CBaseGrenade ) + { + if ( !ent.IsPlayer() || ent.IsTitan() ) //Beam should hit cloaked targets, when cloak is updated make IsCloaked() function. + EMPGrenade_ArcBeam( DamageInfo_GetDamagePosition( damageInfo ), ent ) + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// HACK: might make sense to move this to code +void function NpcEmpRebootPrototype( entity npc, var damageInfo, asset humanFx, asset titanFx ) +{ + if ( !IsValid( npc ) ) + return + + npc.EndSignal( "OnDeath" ) + npc.EndSignal( "OnDestroy" ) + + if ( !( "rebooting" in npc.s ) ) + npc.s.rebooting <- null + + if ( npc.s.rebooting ) // npc already knocked down and in rebooting process + return + + float rebootTime + vector groundPos + local nearestNode + local neighborNodes + local groundNodePos + local origin = npc.GetOrigin() + local startOrigin = origin + local classname = npc.GetClassName() + local soundPowerDown + local soundPowerUp + + //------------------------------------------------------ + // Custom stuff depending on AI type + //------------------------------------------------------ + switch ( classname ) + { + case "npc_drone": + soundPowerDown = "Drone_Power_Down" + soundPowerUp = "Drone_Power_On" + rebootTime = DRONE_REBOOT_TIME + break + case "npc_gunship": + soundPowerDown = "Gunship_Power_Down" + soundPowerUp = "Gunship_Power_On" + rebootTime = GUNSHIP_REBOOT_TIME + break + default: + Assert( 0, "Unhandled npc type: " + classname ) + + } + + //------------------------------------------------------ + // NPC stunned and is rebooting + //------------------------------------------------------ + npc.Signal( "OnStunned" ) + npc.s.rebooting = true + + + //TODO: make drone/gunship slowly drift to the ground while rebooting + /* + groundPos = OriginToGround( origin ) + groundPos += Vector( 0, 0, 32 ) + + + //DebugDrawLine(origin, groundPos, 255, 0, 0, true, 15 ) + + //thread AssaultOrigin( drone, groundPos, 16 ) + //thread PlayAnim( drone, "idle" ) + */ + + + thread EmpRebootFxPrototype( npc, humanFx, titanFx ) + npc.EnableNPCFlag( NPC_IGNORE_ALL ) + npc.SetNoTarget( true ) + npc.EnableNPCFlag( NPC_DISABLE_SENSING ) // don't do traces to look for enemies or players + + if ( IsAttackDrone( npc ) ) + npc.SetAttackMode( false ) + + EmitSoundOnEntity( npc, soundPowerDown ) + + wait rebootTime + + EmitSoundOnEntity( npc, soundPowerUp ) + npc.DisableNPCFlag( NPC_IGNORE_ALL ) + npc.SetNoTarget( false ) + npc.DisableNPCFlag( NPC_DISABLE_SENSING ) // don't do traces to look for enemies or players + + if ( IsAttackDrone( npc ) ) + npc.SetAttackMode( true ) + + npc.s.rebooting = false +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// HACK: might make sense to move this to code +function EmpRebootFxPrototype( npc, asset humanFx, asset titanFx ) +{ + expect entity( npc ) + + if ( !IsValid( npc ) ) + return + + npc.EndSignal( "OnDeath" ) + npc.EndSignal( "OnDestroy" ) + + string classname = npc.GetClassName() + vector origin + float delayDuration + entity fxHandle + asset fxEMPdamage + string fxTag + float rebootTime + string soundEMPdamage + + //------------------------------------------------------ + // Custom stuff depending on AI type + //------------------------------------------------------ + switch ( classname ) + { + case "npc_drone": + if ( GetDroneType( npc ) == "drone_type_marvin" ) + return + fxEMPdamage = humanFx + fxTag = "HEADSHOT" + rebootTime = DRONE_REBOOT_TIME + soundEMPdamage = "Titan_Blue_Electricity_Cloud" + break + case "npc_gunship": + fxEMPdamage = titanFx + fxTag = "ORIGIN" + rebootTime = GUNSHIP_REBOOT_TIME + soundEMPdamage = "Titan_Blue_Electricity_Cloud" + break + default: + Assert( 0, "Unhandled npc type: " + classname ) + + } + + //------------------------------------------------------ + // Play Fx/Sound till reboot finishes + //------------------------------------------------------ + fxHandle = ClientStylePlayFXOnEntity( fxEMPdamage, npc, fxTag, rebootTime ) + EmitSoundOnEntity( npc, soundEMPdamage ) + + while ( npc.s.rebooting == true ) + { + delayDuration = RandomFloatRange( 0.4, 1.2 ) + origin = npc.GetOrigin() + + + EmitSoundAtPosition( npc.GetTeam(), origin, SOUND_EMP_REBOOT_SPARKS ) + PlayFX( FX_EMP_REBOOT_SPARKS, origin ) + PlayFX( FX_EMP_REBOOT_SPARKS, origin ) + + OnThreadEnd( + function() : ( fxHandle, npc, soundEMPdamage ) + { + if ( IsValid( fxHandle ) ) + fxHandle.Fire( "StopPlayEndCap" ) + if ( IsValid( npc ) ) + StopSoundOnEntity( npc, soundEMPdamage ) + } + ) + + wait ( delayDuration ) + } +} + +function EMP_FX( asset effect, entity ent, string tag, float duration ) +{ + if ( !IsAlive( ent ) ) + return + + ent.Signal( "EMP_FX" ) + ent.EndSignal( "OnDestroy" ) + ent.EndSignal( "OnDeath" ) + ent.EndSignal( "StartPhaseShift" ) + ent.EndSignal( "EMP_FX" ) + + bool isPlayer = ent.IsPlayer() + + int fxId = GetParticleSystemIndex( effect ) + int attachId = ent.LookupAttachment( tag ) + + entity fxHandle = StartParticleEffectOnEntity_ReturnEntity( ent, fxId, FX_PATTACH_POINT_FOLLOW, attachId ) + fxHandle.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY + fxHandle.SetOwner( ent ) + + OnThreadEnd( + function() : ( fxHandle, ent ) + { + if ( IsValid( fxHandle ) ) + { + EffectStop( fxHandle ) + } + + if ( IsValid( ent ) ) + StopSoundOnEntity( ent, "Titan_Blue_Electricity_Cloud" ) + } + ) + + if ( !isPlayer ) + { + EmitSoundOnEntity( ent, "Titan_Blue_Electricity_Cloud" ) + wait duration + } + else + { + EmitSoundOnEntityExceptToPlayer( ent, ent, "Titan_Blue_Electricity_Cloud" ) + + var endTime = Time() + duration + bool effectsActive = true + while( endTime > Time() ) + { + if ( ent.IsPhaseShifted() ) + { + if ( effectsActive ) + { + effectsActive = false + if ( IsValid( fxHandle ) ) + EffectSleep( fxHandle ) + + if ( IsValid( ent ) ) + StopSoundOnEntity( ent, "Titan_Blue_Electricity_Cloud" ) + } + } + else if ( effectsActive == false ) + { + EffectWake( fxHandle ) + EmitSoundOnEntityExceptToPlayer( ent, ent, "Titan_Blue_Electricity_Cloud" ) + effectsActive = true + } + + WaitFrame() + } + } +} + +function EMPGrenade_AffectsShield( entity titan, damageInfo ) +{ + int shieldHealth = titan.GetTitanSoul().GetShieldHealth() + int shieldDamage = int( titan.GetTitanSoul().GetShieldHealthMax() * 0.5 ) + + titan.GetTitanSoul().SetShieldHealth( maxint( 0, shieldHealth - shieldDamage ) ) + + // attacker took down titan shields + if ( shieldHealth && !titan.GetTitanSoul().GetShieldHealth() ) + { + entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( attacker && attacker.IsPlayer() ) + EmitSoundOnEntityOnlyToPlayer( attacker, attacker, "titan_energyshield_down" ) + } +} + +function EMPGrenade_AffectsAccuracy( npcTitan ) +{ + npcTitan.EndSignal( "OnDestroy" ) + + npcTitan.kv.AccuracyMultiplier = 0.5 + wait EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MAX + npcTitan.kv.AccuracyMultiplier = 1.0 +} + + +function EMPGrenade_EffectsPlayer( entity player, damageInfo ) +{ + player.Signal( "OnEMPPilotHit" ) + player.EndSignal( "OnEMPPilotHit" ) + + if ( player.IsPhaseShifted() ) + return + + entity inflictor = DamageInfo_GetInflictor( damageInfo ) + local dist = Distance( DamageInfo_GetDamagePosition( damageInfo ), player.GetWorldSpaceCenter() ) + local damageRadius = 128 + if ( inflictor instanceof CBaseGrenade ) + damageRadius = inflictor.GetDamageRadius() + float frac = GraphCapped( dist, damageRadius * 0.5, damageRadius, 1.0, 0.0 ) + local strength = EMP_GRENADE_PILOT_SCREEN_EFFECTS_MIN + ( ( EMP_GRENADE_PILOT_SCREEN_EFFECTS_MAX - EMP_GRENADE_PILOT_SCREEN_EFFECTS_MIN ) * frac ) + float fadeoutDuration = EMP_GRENADE_PILOT_SCREEN_EFFECTS_FADE * frac + float duration = EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MIN + ( ( EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MAX - EMP_GRENADE_PILOT_SCREEN_EFFECTS_DURATION_MIN ) * frac ) - fadeoutDuration + local origin = inflictor.GetOrigin() + + int dmgSource = DamageInfo_GetDamageSourceIdentifier( damageInfo ) + if ( dmgSource == eDamageSourceId.mp_weapon_proximity_mine || dmgSource == eDamageSourceId.mp_titanweapon_stun_laser ) + { + strength *= 0.1 + } + + if ( player.IsTitan() ) + { + // Hit player should do EMP screen effects locally + Remote_CallFunction_Replay( player, "ServerCallback_TitanCockpitEMP", duration ) + + EMPGrenade_AffectsShield( player, damageInfo ) + + Remote_CallFunction_Replay( player, "ServerCallback_TitanEMP", strength, duration, fadeoutDuration ) + } + else + { + if ( IsCloaked( player ) ) + player.SetCloakFlicker( 0.5, duration ) + + // duration = 0 + // fadeoutDuration = 0 + + StatusEffect_AddTimed( player, eStatusEffect.emp, strength, duration, fadeoutDuration ) + //DamageInfo_SetDamage( damageInfo, 0 ) + } + + GiveEMPStunStatusEffects( player, (duration + fadeoutDuration), fadeoutDuration) +} + +function EMPGrenade_ArcBeam( grenadePos, ent ) +{ + if ( !ent.IsPlayer() && !ent.IsNPC() ) + return + + Assert( IsValid( ent ) ) + local lifeDuration = 0.5 + + // Control point sets the end position of the effect + entity cpEnd = CreateEntity( "info_placement_helper" ) + SetTargetName( cpEnd, UniqueString( "emp_grenade_beam_cpEnd" ) ) + cpEnd.SetOrigin( grenadePos ) + DispatchSpawn( cpEnd ) + + entity zapBeam = CreateEntity( "info_particle_system" ) + zapBeam.kv.cpoint1 = cpEnd.GetTargetName() + zapBeam.SetValueForEffectNameKey( EMP_GRENADE_BEAM_EFFECT ) + zapBeam.kv.start_active = 0 + zapBeam.SetOrigin( ent.GetWorldSpaceCenter() ) + if ( !ent.IsMarkedForDeletion() ) // TODO: This is a hack for shipping. Should not be parenting to deleted entities + { + zapBeam.SetParent( ent, "", true, 0.0 ) + } + + DispatchSpawn( zapBeam ) + + zapBeam.Fire( "Start" ) + zapBeam.Fire( "StopPlayEndCap", "", lifeDuration ) + zapBeam.Kill_Deprecated_UseDestroyInstead( lifeDuration ) + cpEnd.Kill_Deprecated_UseDestroyInstead( lifeDuration ) +} + +void function GetWeaponDPS( bool vsTitan = false ) +{ + entity player = GetPlayerArray()[0] + entity weapon = player.GetActiveWeapon() + + local fire_rate = weapon.GetWeaponInfoFileKeyField( "fire_rate" ) + local burst_fire_count = weapon.GetWeaponInfoFileKeyField( "burst_fire_count" ) + local burst_fire_delay = weapon.GetWeaponInfoFileKeyField( "burst_fire_delay" ) + + local damage_near_value = weapon.GetWeaponInfoFileKeyField( "damage_near_value" ) + local damage_far_value = weapon.GetWeaponInfoFileKeyField( "damage_far_value" ) + + if ( vsTitan ) + { + damage_near_value = weapon.GetWeaponInfoFileKeyField( "damage_near_value_titanarmor" ) + damage_far_value = weapon.GetWeaponInfoFileKeyField( "damage_far_value_titanarmor" ) + } + + if ( burst_fire_count ) + { + local timePerShot = 1 / fire_rate + local timePerBurst = (timePerShot * burst_fire_count) + burst_fire_delay + local burstPerSecond = 1 / timePerBurst + + printt( timePerBurst ) + + printt( "DPS Near", (burstPerSecond * burst_fire_count) * damage_near_value ) + printt( "DPS Far ", (burstPerSecond * burst_fire_count) * damage_far_value ) + } + else + { + printt( "DPS Near", fire_rate * damage_near_value ) + printt( "DPS Far ", fire_rate * damage_far_value ) + } +} + + +void function GetTTK( string weaponRef, float health = 100.0 ) +{ + local fire_rate = GetWeaponInfoFileKeyField_Global( weaponRef, "fire_rate" ).tofloat() + local burst_fire_count = GetWeaponInfoFileKeyField_Global( weaponRef, "burst_fire_count" ) + if ( burst_fire_count != null ) + burst_fire_count = burst_fire_count.tofloat() + + local burst_fire_delay = GetWeaponInfoFileKeyField_Global( weaponRef, "burst_fire_delay" ) + if ( burst_fire_delay != null ) + burst_fire_delay = burst_fire_delay.tofloat() + + local damage_near_value = GetWeaponInfoFileKeyField_Global( weaponRef, "damage_near_value" ).tointeger() + local damage_far_value = GetWeaponInfoFileKeyField_Global( weaponRef, "damage_far_value" ).tointeger() + + local nearBodyShots = ceil( health / damage_near_value ) - 1 + local farBodyShots = ceil( health / damage_far_value ) - 1 + + local delayAdd = 0 + if ( burst_fire_count && burst_fire_count < nearBodyShots ) + delayAdd += burst_fire_delay + + printt( "TTK Near", (nearBodyShots * (1 / fire_rate)) + delayAdd, " (" + (nearBodyShots + 1) + ")" ) + + + delayAdd = 0 + if ( burst_fire_count && burst_fire_count < farBodyShots ) + delayAdd += burst_fire_delay + + printt( "TTK Far ", (farBodyShots * (1 / fire_rate)) + delayAdd, " (" + (farBodyShots + 1) + ")" ) +} + +array function GetWeaponModsFromDamageInfo( var damageInfo ) +{ + entity weapon = DamageInfo_GetWeapon( damageInfo ) + entity inflictor = DamageInfo_GetInflictor( damageInfo ) + int damageType = DamageInfo_GetCustomDamageType( damageInfo ) + + if ( IsValid( weapon ) ) + { + return weapon.GetMods() + } + else if ( IsValid( inflictor ) ) + { + if ( "weaponMods" in inflictor.s && inflictor.s.weaponMods ) + { + array temp + foreach ( string mod in inflictor.s.weaponMods ) + { + temp.append( mod ) + } + + return temp + } + else if( inflictor.IsProjectile() ) + return inflictor.ProjectileGetMods() + else if ( damageType & DF_EXPLOSION && inflictor.IsPlayer() && IsValid( inflictor.GetActiveWeapon() ) ) + return inflictor.GetActiveWeapon().GetMods() + //Hack - Splash damage doesn't pass mod weapon through. This only works under the assumption that offhand weapons don't have mods. + } + return [] +} + +void function OnPlayerGetsNewPilotLoadout( entity player, PilotLoadoutDef loadout ) +{ + if ( GetCurrentPlaylistVarInt( "featured_mode_amped_tacticals", 0 ) >= 1 ) + { + player.GiveExtraWeaponMod( "amped_tacticals" ) + } + + if ( GetCurrentPlaylistVarInt( "featured_mode_all_grapple", 0 ) >= 1 ) + { + player.GiveExtraWeaponMod( "all_grapple" ) + } + + if ( GetCurrentPlaylistVarInt( "featured_mode_all_phase", 0 ) >= 1 ) + { + player.GiveExtraWeaponMod( "all_phase" ) + } + + SetPlayerCooldowns( player ) +} + +void function SetPlayerCooldowns( entity player ) +{ + if ( player.IsTitan() ) + return + + array offhandIndices = [ OFFHAND_LEFT, OFFHAND_RIGHT ] + + foreach ( index in offhandIndices ) + { + float lastUseTime = player.p.lastPilotOffhandUseTime[ index ] + float lastChargeFrac = player.p.lastPilotOffhandChargeFrac[ index ] + float lastClipFrac = player.p.lastPilotClipFrac[ index ] + + if ( lastUseTime >= 0.0 ) + { + entity weapon = player.GetOffhandWeapon( index ) + if ( !IsValid( weapon ) ) + continue + + string weaponClassName = weapon.GetWeaponClassName() + + switch ( GetWeaponInfoFileKeyField_Global( weaponClassName, "cooldown_type" ) ) + { + case "grapple": + // GetPlayerSettingsField isn't working for moddable fields? - Bug 129567 + float powerRequired = 100.0 // GetPlayerSettingsField( "grapple_power_required" ) + float regenRefillDelay = 3.0 // GetPlayerSettingsField( "grapple_power_regen_delay" ) + float regenRefillRate = 5.0 // GetPlayerSettingsField( "grapple_power_regen_rate" ) + float suitPowerToRestore = powerRequired - player.p.lastSuitPower + float regenRefillTime = suitPowerToRestore / regenRefillRate + + float regenStartTime = lastUseTime + regenRefillDelay + + float newSuitPower = GraphCapped( Time() - regenStartTime, 0.0, regenRefillTime, player.p.lastSuitPower, powerRequired ) + + player.SetSuitGrapplePower( newSuitPower ) + break + + case "ammo": + case "ammo_instant": + case "ammo_deployed": + case "ammo_timed": + int maxAmmo = weapon.GetWeaponPrimaryClipCountMax() + float fireDuration = weapon.GetWeaponSettingFloat( eWeaponVar.fire_duration ) + float regenRefillDelay = weapon.GetWeaponSettingFloat( eWeaponVar.regen_ammo_refill_start_delay ) + float regenRefillRate = weapon.GetWeaponSettingFloat( eWeaponVar.regen_ammo_refill_rate ) + int startingClipCount = int( lastClipFrac * maxAmmo ) + int ammoToRestore = maxAmmo - startingClipCount + float regenRefillTime = ammoToRestore / regenRefillRate + + float regenStartTime = lastUseTime + fireDuration + regenRefillDelay + + int newAmmo = int( GraphCapped( Time() - regenStartTime, 0.0, regenRefillTime, startingClipCount, maxAmmo ) ) + + weapon.SetWeaponPrimaryClipCountAbsolute( newAmmo ) + break + + case "chargeFrac": + float chargeCooldownDelay = weapon.GetWeaponSettingFloat( eWeaponVar.charge_cooldown_delay ) + float chargeCooldownTime = weapon.GetWeaponSettingFloat( eWeaponVar.charge_cooldown_time ) + float regenRefillTime = lastChargeFrac * chargeCooldownTime + float regenStartTime = lastUseTime + chargeCooldownDelay + + float newCharge = GraphCapped( Time() - regenStartTime, 0.0, regenRefillTime, lastChargeFrac, 0.0 ) + + weapon.SetWeaponChargeFraction( newCharge ) + break + + default: + printt( weaponClassName + " needs to be updated to support cooldown_type setting" ) + break + } + } + } +} + +void function ResetPlayerCooldowns( entity player ) +{ + if ( player.IsTitan() ) + return + + array offhandIndices = [ OFFHAND_LEFT, OFFHAND_RIGHT ] + + foreach ( index in offhandIndices ) + { + float lastUseTime = -99.0//player.p.lastPilotOffhandUseTime[ index ] + float lastChargeFrac = -1.0//player.p.lastPilotOffhandChargeFrac[ index ] + float lastClipFrac = 1.0//player.p.lastPilotClipFrac[ index ] + + entity weapon = player.GetOffhandWeapon( index ) + if ( !IsValid( weapon ) ) + continue + + string weaponClassName = weapon.GetWeaponClassName() + + switch ( GetWeaponInfoFileKeyField_Global( weaponClassName, "cooldown_type" ) ) + { + case "grapple": + // GetPlayerSettingsField isn't working for moddable fields? - Bug 129567 + float powerRequired = 100.0 // GetPlayerSettingsField( "grapple_power_required" ) + player.SetSuitGrapplePower( powerRequired ) + break + + case "ammo": + case "ammo_instant": + case "ammo_deployed": + case "ammo_timed": + int maxAmmo = weapon.GetWeaponPrimaryClipCountMax() + weapon.SetWeaponPrimaryClipCountAbsolute( maxAmmo ) + break + + case "chargeFrac": + weapon.SetWeaponChargeFraction( 1.0 ) + break + + default: + printt( weaponClassName + " needs to be updated to support cooldown_type setting" ) + break + } + } +} + +void function OnPlayerKilled( entity player, entity attacker, var damageInfo ) +{ + StoreOffhandData( player ) +} + +void function StoreOffhandData( entity player, bool waitEndFrame = true ) +{ + thread StoreOffhandDataThread( player, waitEndFrame ) +} + +void function StoreOffhandDataThread( entity player, bool waitEndFrame ) +{ + if ( !IsValid( player ) ) + return + + player.EndSignal( "OnDestroy" ) + + if ( waitEndFrame ) + WaitEndFrame() // Need to WaitEndFrame so clip counts can be updated if player is dying the same frame + + array offhandIndices = [ OFFHAND_LEFT, OFFHAND_RIGHT ] + + // Reset all values for full cooldown + player.p.lastSuitPower = 0.0 + + foreach ( index in offhandIndices ) + { + player.p.lastPilotOffhandChargeFrac[ index ] = 1.0 + player.p.lastPilotClipFrac[ index ] = 1.0 + + player.p.lastTitanOffhandChargeFrac[ index ] = 1.0 + player.p.lastTitanClipFrac[ index ] = 1.0 + } + + if ( player.IsTitan() ) + return + + foreach ( index in offhandIndices ) + { + entity weapon = player.GetOffhandWeapon( index ) + if ( !IsValid( weapon ) ) + continue + + string weaponClassName = weapon.GetWeaponClassName() + + switch ( GetWeaponInfoFileKeyField_Global( weaponClassName, "cooldown_type" ) ) + { + case "grapple": + player.p.lastSuitPower = player.GetSuitGrapplePower() + break + + case "ammo": + case "ammo_instant": + case "ammo_deployed": + case "ammo_timed": + + if ( player.IsTitan() ) + { + if ( !weapon.IsWeaponRegenDraining() ) + player.p.lastTitanClipFrac[ index ] = min( 1.0, weapon.GetWeaponPrimaryClipCount() / float( weapon.GetWeaponPrimaryClipCountMax() ) ) //Was returning greater than one with extraweaponmod timing. + else + player.p.lastTitanClipFrac[ index ] = 0.0 + } + else + { + if ( !weapon.IsWeaponRegenDraining() ) + player.p.lastPilotClipFrac[ index ] = min( 1.0, weapon.GetWeaponPrimaryClipCount() / float( weapon.GetWeaponPrimaryClipCountMax() ) ) //Was returning greater than one with extraweaponmod timing. + else + player.p.lastPilotClipFrac[ index ] = 0.0 + } + break + + case "chargeFrac": + if ( player.IsTitan() ) + player.p.lastTitanOffhandChargeFrac[ index ] = weapon.GetWeaponChargeFraction() + else + player.p.lastPilotOffhandChargeFrac[ index ] = weapon.GetWeaponChargeFraction() + break + + default: + printt( weaponClassName + " needs to be updated to support cooldown_type setting" ) + break + } + } +} +#endif // #if SERVER + +void function PlayerUsedOffhand( entity player, entity offhandWeapon ) +{ + array offhandIndices = [ OFFHAND_LEFT, OFFHAND_RIGHT, OFFHAND_ANTIRODEO, OFFHAND_EQUIPMENT ] + + foreach ( index in offhandIndices ) + { + entity weapon = player.GetOffhandWeapon( index ) + if ( !IsValid( weapon ) ) + continue + + if ( weapon != offhandWeapon ) + continue + + #if SERVER + if ( player.IsTitan() ) + player.p.lastTitanOffhandUseTime[ index ] = Time() + else + player.p.lastPilotOffhandUseTime[ index ] = Time() + + #if MP + string weaponName = offhandWeapon.GetWeaponClassName() + if ( weaponName != "mp_ability_grapple" ) // handled in CodeCallback_OnGrapple // nope, it's not (?) + { + string category + float duration + if ( index == OFFHAND_EQUIPMENT && player.IsTitan() ) + { + category = "core" + duration = -1 + } + else + { + category = "" + duration = Time() - offhandWeapon.GetNextAttackAllowedTimeRaw() + } + PIN_PlayerAbility( player, category, weaponName, {}, duration ) + } + #endif + #endif // SERVER + + #if HAS_TITAN_TELEMETRY && CLIENT + ClTitanHints_ClearOffhandHint( index ) + #endif + + #if HAS_TITAN_TELEMETRY && SERVER + TitanHints_NotifyUsedOffhand( index ) + #endif + + return + } +} + +RadiusDamageData function GetRadiusDamageDataFromProjectile( entity projectile, entity owner ) +{ + RadiusDamageData radiusDamageData + + radiusDamageData.explosionDamage = -1 + radiusDamageData.explosionDamageHeavyArmor = -1 + + if ( owner.IsNPC() ) + { + radiusDamageData.explosionDamage = projectile.GetProjectileWeaponSettingInt( eWeaponVar.npc_explosion_damage ) + radiusDamageData.explosionDamageHeavyArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.npc_explosion_damage_heavy_armor ) + } + + if ( radiusDamageData.explosionDamage == -1 ) + radiusDamageData.explosionDamage = projectile.GetProjectileWeaponSettingInt( eWeaponVar.explosion_damage ) + + if ( radiusDamageData.explosionDamageHeavyArmor == -1 ) + radiusDamageData.explosionDamageHeavyArmor = projectile.GetProjectileWeaponSettingInt( eWeaponVar.explosion_damage_heavy_armor ) + + radiusDamageData.explosionRadius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosionradius ) + radiusDamageData.explosionInnerRadius = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.explosion_inner_radius ) + + Assert( radiusDamageData.explosionRadius > 0, "Created RadiusDamageData with 0 radius" ) + Assert( radiusDamageData.explosionDamage > 0 || radiusDamageData.explosionDamageHeavyArmor > 0, "Created RadiusDamageData with 0 damage" ) + return radiusDamageData +} + +#if SERVER +void function Thermite_DamagePlayerOrNPCSounds( entity ent ) +{ + if ( ent.IsTitan() ) + { + if ( ent.IsPlayer() ) + { + EmitSoundOnEntityOnlyToPlayer( ent, ent, "titan_thermiteburn_3p_vs_1p" ) + EmitSoundOnEntityExceptToPlayer( ent, ent, "titan_thermiteburn_1p_vs_3p" ) + } + else + { + EmitSoundOnEntity( ent, "titan_thermiteburn_1p_vs_3p" ) + } + } + else + { + if ( ent.IsPlayer() ) + { + EmitSoundOnEntityOnlyToPlayer( ent, ent, "flesh_thermiteburn_3p_vs_1p" ) + EmitSoundOnEntityExceptToPlayer( ent, ent, "flesh_thermiteburn_1p_vs_3p" ) + } + else + { + EmitSoundOnEntity( ent, "flesh_thermiteburn_1p_vs_3p" ) + } + } +} +#endif + +#if SERVER +void function RemoveThreatScopeColorStatusEffect( entity player ) +{ + for ( int i = file.colorSwapStatusEffects.len() - 1; i >= 0; i-- ) + { + entity owner = file.colorSwapStatusEffects[i].weaponOwner + if ( !IsValid( owner ) ) + { + file.colorSwapStatusEffects.remove( i ) + continue + } + if ( owner == player ) + { + StatusEffect_Stop( player, file.colorSwapStatusEffects[i].statusEffectId ) + file.colorSwapStatusEffects.remove( i ) + } + } +} + +void function AddThreatScopeColorStatusEffect( entity player ) +{ + ColorSwapStruct info + info.weaponOwner = player + info.statusEffectId = StatusEffect_AddTimed( player, eStatusEffect.cockpitColor, COCKPIT_COLOR_THREAT, 100000, 0 ) + file.colorSwapStatusEffects.append( info ) +} +#endif -- cgit v1.2.3