aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/scripts/vscripts/weapons
diff options
context:
space:
mode:
authorBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-06-22 14:30:49 +0100
committerBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-06-22 14:30:49 +0100
commit207facbc402f5639cbcd31f079214351ef605cf2 (patch)
tree4710b2a88dd64f3dfea1609d31a5de9141640951 /Northstar.CustomServers/scripts/vscripts/weapons
parentc2d438568df6d98cf731807e30eaa7da31e5ea52 (diff)
downloadNorthstarMods-207facbc402f5639cbcd31f079214351ef605cf2.tar.gz
NorthstarMods-207facbc402f5639cbcd31f079214351ef605cf2.zip
initial commit after moving to new repo
Diffstat (limited to 'Northstar.CustomServers/scripts/vscripts/weapons')
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_arc_cannon.nut1032
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_at_turrets.gnut284
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_ball_lightning.gnut363
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_cloaker.gnut121
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_grenade.nut604
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_particle_wall.gnut460
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_team_emp.gnut38
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_vortex.nut1983
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_weapon_dialogue.nut44
-rw-r--r--Northstar.CustomServers/scripts/vscripts/weapons/_weapon_utility.nut3966
10 files changed, 8895 insertions, 0 deletions
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_arc_cannon.nut b/Northstar.CustomServers/scripts/vscripts/weapons/_arc_cannon.nut
new file mode 100644
index 00000000..1601330c
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_arc_cannon.nut
@@ -0,0 +1,1032 @@
+untyped
+
+global function ArcCannon_Init
+
+global function ArcCannon_PrecacheFX
+global function ArcCannon_Start
+global function ArcCannon_Stop
+global function ArcCannon_ChargeBegin
+global function ArcCannon_ChargeEnd
+global function FireArcCannon
+global function ArcCannon_HideIdleEffect
+#if SERVER
+ global function AddToArcCannonTargets
+ global function RemoveArcCannonTarget
+ global function ConvertTitanShieldIntoBonusCharge
+#endif
+global function GetArcCannonChargeFraction
+
+global function IsEntANeutralMegaTurret
+global function CreateArcCannonBeam
+
+
+// Aiming & Range
+global const DEFAULT_ARC_CANNON_FOVDOT = 0.98 // First target must be within this dot to be zapped and start a chain
+global const DEFAULT_ARC_CANNON_FOVDOT_MISSILE = 0.95 // First target must be within this dot to be zapped and start a chain ( if it's a missile, we allow more leaniency )
+global const ARC_CANNON_RANGE_CHAIN = 400 // Max distance we can arc from one target to another
+global const ARC_CANNON_TITAN_RANGE_CHAIN = 900 // Max distance we can arc from one target to another
+global const ARC_CANNON_CHAIN_COUNT_MIN = 5 // Max number of chains at no charge
+global const ARC_CANNON_CHAIN_COUNT_MAX = 5 // Max number of chains at full charge
+global const ARC_CANNON_CHAIN_COUNT_NPC = 2 // Number of chains when an NPC fires the weapon
+global const ARC_CANNON_FORK_COUNT_MAX = 1 // Number of forks that can come out of one target to other targets
+global const ARC_CANNON_FORK_DELAY = 0.1
+
+global const ARC_CANNON_RANGE_CHAIN_BURN = 400
+global const ARC_CANNON_TITAN_RANGE_CHAIN_BURN = 900
+global const ARC_CANNON_CHAIN_COUNT_MIN_BURN = 100 // Max number of chains at no charge
+global const ARC_CANNON_CHAIN_COUNT_MAX_BURN = 100 // Max number of chains at full charge
+global const ARC_CANNON_CHAIN_COUNT_NPC_BURN = 10 // Number of chains when an NPC fires the weapon
+global const ARC_CANNON_FORK_COUNT_MAX_BURN = 10 // Number of forks that can come out of one target to other targets
+global const ARC_CANNON_BEAM_LIFETIME_BURN = 1
+
+// Visual settings
+global const ARC_CANNON_BOLT_RADIUS_MIN = 32 // Bolt radius at no charge ( not actually sure what this does to the beam lol )
+global const ARC_CANNON_BOLT_RADIUS_MAX = 640 // Bold radius at full charge ( not actually sure what this does to the beam lol )
+global const ARC_CANNON_BOLT_WIDTH_MIN = 1 // Bolt width at no charge
+global const ARC_CANNON_BOLT_WIDTH_MAX = 26 // Bolt width at full charge
+global const ARC_CANNON_BOLT_WIDTH_NPC = 8 // Bolt width when used by NPC
+global const ARC_CANNON_BEAM_COLOR = "150 190 255"
+global const ARC_CANNON_BEAM_LIFETIME = 0.75
+
+// Player Effects
+global const ARC_CANNON_TITAN_SCREEN_SFX = "Null_Remove_SoundHook"
+global const ARC_CANNON_PILOT_SCREEN_SFX = "Null_Remove_SoundHook"
+global const ARC_CANNON_EMP_DURATION_MIN = 0.1
+global const ARC_CANNON_EMP_DURATION_MAX = 1.8
+global const ARC_CANNON_EMP_FADEOUT_DURATION = 0.4
+global const ARC_CANNON_SCREEN_EFFECTS_MIN = 0.01
+global const ARC_CANNON_SCREEN_EFFECTS_MAX = 0.02
+global const ARC_CANNON_SCREEN_THRESHOLD = 0.3385
+global const ARC_CANNON_3RD_PERSON_EFFECT_MIN_DURATION = 0.2
+
+// Damage
+global const ARC_CANNON_DAMAGE_FALLOFF_SCALER = 0.75 // Amount of damage carried on to the next target in the chain lightning. If 0.75, then a target that would normally take 100 damage will take 75 damage if they are one chain deep, or 56 damage if 2 levels deep
+global const ARC_CANNON_DAMAGE_CHARGE_RATIO = 0.85 // What amount of charge is required for full damage.
+global const ARC_CANNON_DAMAGE_CHARGE_RATIO_BURN = 0.676 // What amount of charge is required for full damage.
+global const ARC_CANNON_CAPACITOR_CHARGE_RATIO = 1.0
+
+// Options
+global const ARC_CANNON_TARGETS_MISSILES = 1 // 1 = arc cannon zaps missiles that are active, 0 = missiles are ignored by arc cannon
+
+//Mods
+global const OVERCHARGE_MAX_SHIELD_DECAY = 0.2
+global const OVERCHARGE_SHIELD_DECAY_MULTIPLIER = 0.04
+global const OVERCHARGE_BONUS_CHARGE_FRACTION = 0.05
+
+global const SPLITTER_DAMAGE_FALLOFF_SCALER = 0.6
+global const SPLITTER_FORK_COUNT_MAX = 10
+
+global const ARC_CANNON_SIGNAL_DEACTIVATED = "ArcCannonDeactivated"
+global const ARC_CANNON_SIGNAL_CHARGEEND = "ArcCannonChargeEnd"
+
+global const ARC_CANNON_BEAM_EFFECT = $"wpn_arc_cannon_beam"
+global const ARC_CANNON_BEAM_EFFECT_MOD = $"wpn_arc_cannon_beam_mod"
+
+global const ARC_CANNON_FX_TABLE = "exp_arc_cannon"
+
+global const ArcCannonTargetClassnames = {
+ [ "npc_drone" ] = true,
+ [ "npc_dropship" ] = true,
+ [ "npc_marvin" ] = true,
+ [ "npc_prowler" ] = true,
+ [ "npc_soldier" ] = true,
+ [ "npc_soldier_heavy" ] = true,
+ [ "npc_soldier_shield" ] = true,
+ [ "npc_spectre" ] = true,
+ [ "npc_stalker" ] = true,
+ [ "npc_super_spectre" ] = true,
+ [ "npc_titan" ] = true,
+ [ "npc_turret_floor" ] = true,
+ [ "npc_turret_mega" ] = true,
+ [ "npc_turret_sentry" ] = true,
+ [ "npc_frag_drone" ] = true,
+ [ "player" ] = true,
+ [ "prop_dynamic" ] = true,
+ [ "prop_script" ] = true,
+ [ "grenade_frag" ] = true,
+ [ "rpg_missile" ] = true,
+ [ "script_mover" ] = true,
+ [ "turret" ] = true,
+}
+
+struct {
+ array<string> missileCheckTargetnames = [
+ // "Arc Pylon",
+ "Arc Ball"
+ ]
+} file;
+
+function ArcCannon_Init()
+{
+ RegisterSignal( ARC_CANNON_SIGNAL_DEACTIVATED )
+ RegisterSignal( ARC_CANNON_SIGNAL_CHARGEEND )
+ PrecacheParticleSystem( ARC_CANNON_BEAM_EFFECT )
+ PrecacheParticleSystem( ARC_CANNON_BEAM_EFFECT_MOD )
+ PrecacheImpactEffectTable( ARC_CANNON_FX_TABLE )
+
+ #if CLIENT
+ AddDestroyCallback( "mp_titanweapon_arc_cannon", ClientDestroyCallback_ArcCannon_Stop )
+ #else
+ level._arcCannonTargetsArrayID <- CreateScriptManagedEntArray()
+ #endif
+
+ PrecacheParticleSystem( $"impact_arc_cannon_titan" )
+}
+
+function ArcCannon_PrecacheFX()
+{
+ PrecacheParticleSystem( $"wpn_arc_cannon_electricity_fp" )
+ PrecacheParticleSystem( $"wpn_arc_cannon_electricity" )
+
+ PrecacheParticleSystem( $"wpn_muzzleflash_arc_cannon_fp" )
+ PrecacheParticleSystem( $"wpn_muzzleflash_arc_cannon" )
+}
+
+function ArcCannon_Start( weapon )
+{
+ expect entity( weapon )
+ if ( !IsPilot( weapon.GetWeaponOwner() ) )
+ {
+ weapon.PlayWeaponEffectNoCull( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity", "muzzle_flash" )
+ weapon.EmitWeaponSound( "arc_cannon_charged_loop" )
+ }
+ else
+ {
+ weapon.EmitWeaponSound_1p3p( "Arc_Rifle_charged_Loop_1P", "Arc_Rifle_charged_Loop_3P" )
+ }
+}
+
+function ArcCannon_Stop( weapon, player = null )
+{
+ expect entity( weapon )
+ weapon.Signal( ARC_CANNON_SIGNAL_DEACTIVATED )
+
+ weapon.StopWeaponEffect( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity" )
+ weapon.StopWeaponSound( "arc_cannon_charged_loop" )
+}
+
+function ArcCannon_ChargeBegin( entity weapon )
+{
+ #if SERVER
+ if ( weapon.HasMod( "overcharge" ) )
+ {
+ entity weaponOwner = weapon.GetWeaponOwner()
+ if ( weaponOwner.IsTitan() )
+ {
+ entity soul = weaponOwner.GetTitanSoul()
+ thread ConvertTitanShieldIntoBonusCharge( soul, weapon )
+ }
+ }
+ #endif
+
+ #if CLIENT
+ if ( !weapon.ShouldPredictProjectiles() )
+ return
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ Assert( weaponOwner.IsPlayer() )
+ weaponOwner.StartArcCannon();
+ #endif
+}
+
+function ArcCannon_ChargeEnd( entity weapon, entity player = null )
+{
+ #if SERVER
+ if ( IsValid( weapon ) )
+ weapon.Signal( ARC_CANNON_SIGNAL_CHARGEEND )
+ #endif
+
+ #if CLIENT
+ if ( weapon.GetWeaponOwner() == GetLocalViewPlayer() )
+ {
+ entity weaponOwner
+ if ( player != null )
+ weaponOwner = player
+ else
+ weaponOwner = weapon.GetWeaponOwner()
+
+ if ( IsValid( weaponOwner ) && weaponOwner.IsPlayer() )
+ weaponOwner.StopArcCannon()
+ }
+ #endif
+}
+
+#if SERVER
+function ConvertTitanShieldIntoBonusCharge( entity soul, entity weapon )
+{
+ weapon.EndSignal( ARC_CANNON_SIGNAL_CHARGEEND )
+ weapon.EndSignal( "OnDestroy" )
+
+ local maxShieldDecay = OVERCHARGE_MAX_SHIELD_DECAY
+ local bonusChargeFraction = OVERCHARGE_BONUS_CHARGE_FRACTION
+ local shieldDecayMultiplier = OVERCHARGE_SHIELD_DECAY_MULTIPLIER
+ int shieldHealthMax = soul.GetShieldHealthMax()
+ local chargeRatio = GetArcCannonChargeFraction( weapon )
+
+ while( 1 )
+ {
+ if ( !IsValid( soul ) || !IsValid( weapon ) )
+ break
+
+ local baseCharge = GetWeaponChargeFrac( weapon ) // + GetOverchargeBonusChargeFraction()
+ local charge = clamp ( baseCharge * ( 1 / chargeRatio ), 0.0, 1.0 )
+ if ( charge < 1.0 || maxShieldDecay > 0)
+ {
+ int shieldHealth = soul.GetShieldHealth()
+
+ //Slight inconsistency in server updates, this ensures it never takes too much.
+ if ( shieldDecayMultiplier > maxShieldDecay )
+ shieldDecayMultiplier = maxShieldDecay
+ maxShieldDecay -= shieldDecayMultiplier
+
+ local shieldDecayAmount = shieldHealthMax * shieldDecayMultiplier
+ local newShieldAmount = shieldHealth - shieldDecayAmount
+ soul.SetShieldHealth( max( newShieldAmount, 0 ) )
+ soul.nextRegenTime = Time() + GetShieldRegenTime( soul )
+
+ if ( shieldDecayAmount > shieldHealth )
+ bonusChargeFraction = bonusChargeFraction * ( shieldHealth / shieldDecayAmount )
+ weapon.SetWeaponChargeFraction( baseCharge + bonusChargeFraction )
+ }
+ wait 0.1
+ }
+}
+#endif
+
+function FireArcCannon( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ local weaponScriptScope = weapon.GetScriptScope()
+ local baseCharge = GetWeaponChargeFrac( weapon ) // + GetOverchargeBonusChargeFraction()
+ local charge = clamp( baseCharge * ( 1 / GetArcCannonChargeFraction( weapon ) ), 0.0, 1.0 )
+ float newVolume = GraphCapped( charge, 0.25, 1.0, 0.0, 1.0 )
+
+ weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 )
+
+ weapon.PlayWeaponEffect( $"wpn_muzzleflash_arc_cannon_fp", $"wpn_muzzleflash_arc_cannon", "muzzle_flash" )
+
+ local attachmentName = "muzzle_flash"
+ local attachmentIndex = weapon.LookupAttachment( attachmentName )
+ Assert( attachmentIndex >= 0 )
+ local muzzleOrigin = weapon.GetAttachmentOrigin( attachmentIndex )
+
+ //printt( "-------- FIRING ARC CANNON --------" )
+
+ table firstTargetInfo = GetFirstArcCannonTarget( weapon, attackParams )
+ if ( !IsValid( firstTargetInfo.target ) )
+ FireArcNoTargets( weapon, attackParams, muzzleOrigin )
+ else
+ FireArcWithTargets( weapon, firstTargetInfo, attackParams, muzzleOrigin )
+
+ return 1
+}
+
+table function GetFirstArcCannonTarget( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ entity owner = weapon.GetWeaponOwner()
+ local coneHeight = weapon.GetMaxDamageFarDist()
+
+ local angleToAxis = 2 // set this too high and auto-titans using it will error on FindVisibleEntitiesInCone
+ array<entity> ignoredEntities = [ owner, weapon ]
+ int traceMask = TRACE_MASK_SHOT
+ int flags = VIS_CONE_ENTS_TEST_HITBOXES
+ local antilagPlayer = null
+ if ( owner.IsPlayer() )
+ {
+ angleToAxis = owner.GetAttackSpreadAngle() * 0.11
+ antilagPlayer = owner
+ }
+
+ int ownerTeam = owner.GetTeam()
+
+ // Get a missile target and a non-missile target in the cone that the player can zap
+ // We do this in a separate check so we can use a wider cone to be more forgiving for targeting missiles
+ table firstTargetInfo = {}
+ firstTargetInfo.target <- null
+ firstTargetInfo.hitLocation <- null
+
+ for ( int i = 0; i < 2; i++ )
+ {
+ local missileCheck = i == 0
+ local coneAngle = angleToAxis
+ if ( missileCheck && owner.IsPlayer() ) // missile check only if owner is player
+ coneAngle *= 8.0
+
+ coneAngle = clamp( coneAngle, 0.1, 89.9 )
+
+ array<VisibleEntityInCone> results = FindVisibleEntitiesInCone( attackParams.pos, attackParams.dir, coneHeight, coneAngle, ignoredEntities, traceMask, flags, antilagPlayer )
+ foreach ( result in results )
+ {
+ entity visibleEnt = result.ent
+
+ if ( !IsValid( visibleEnt ) )
+ continue
+
+ if ( visibleEnt.IsPhaseShifted() )
+ continue
+
+ local classname = IsServer() ? visibleEnt.GetClassName() : visibleEnt.GetSignifierName()
+
+ if ( !( classname in ArcCannonTargetClassnames ) )
+ continue
+
+ if ( "GetTeam" in visibleEnt )
+ {
+ int visibleEntTeam = visibleEnt.GetTeam()
+ if ( visibleEntTeam == ownerTeam )
+ continue
+ if ( IsEntANeutralMegaTurret( visibleEnt, ownerTeam ) )
+ continue
+ }
+
+ expect string( classname )
+ string targetname = visibleEnt.GetTargetName()
+
+ if ( missileCheck && ( classname != "rpg_missile" && !file.missileCheckTargetnames.contains( targetname ) ) )
+ continue
+
+ if ( !missileCheck && ( classname == "rpg_missile" || file.missileCheckTargetnames.contains( targetname ) ) )
+ continue
+
+ firstTargetInfo.target = visibleEnt
+ firstTargetInfo.hitLocation = result.visiblePosition
+ break
+ }
+ }
+ //Creating a whiz-by sound.
+ weapon.FireWeaponBullet_Special( attackParams.pos, attackParams.dir, 1, 0, true, true, true, true, true, false, false )
+
+ return firstTargetInfo
+}
+
+function FireArcNoTargets( entity weapon, WeaponPrimaryAttackParams attackParams, muzzleOrigin )
+{
+ Assert( IsValid( weapon ) )
+ entity player = weapon.GetWeaponOwner()
+ local chargeFrac = GetWeaponChargeFrac( weapon )
+ local beamVec = attackParams.dir * weapon.GetMaxDamageFarDist()
+ local playerEyePos = player.EyePosition()
+ TraceResults traceResults = TraceLineHighDetail( playerEyePos, (playerEyePos + beamVec), weapon, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+ local beamEnd = traceResults.endPos
+
+ VortexBulletHit ornull vortexHit = VortexBulletHitCheck( player, playerEyePos, beamEnd )
+ if ( vortexHit )
+ {
+ expect VortexBulletHit( vortexHit )
+ #if SERVER
+ entity vortexWeapon = vortexHit.vortex.GetOwnerWeapon()
+ string className = vortexWeapon.GetWeaponClassName()
+ if ( vortexWeapon && ( className == "mp_titanweapon_vortex_shield" || className == "mp_titanweapon_vortex_shield_ion" ) )
+ {
+ // drain the vortex shield
+ VortexDrainedByImpact( vortexWeapon, weapon, null, null )
+ }
+ else if ( IsVortexSphere( vortexHit.vortex ) )
+ {
+ // do damage to vortex_sphere entities that isn't the titan "vortex shield"
+ local damageNear = weapon.GetWeaponInfoFileKeyField( "damage_near_value" )
+ local damage = damageNear * GraphCapped( chargeFrac, 0, 0.5, 0.0, 1.0 ) * 10 // do more damage the more charged the weapon is.
+ VortexSphereDrainHealthForDamage( vortexHit.vortex, damage )
+ }
+ #endif
+ beamEnd = vortexHit.hitPos
+ }
+
+ float radius = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_RADIUS_MIN, ARC_CANNON_BOLT_RADIUS_MAX )
+ local boltWidth = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_WIDTH_MIN, ARC_CANNON_BOLT_WIDTH_MAX )
+ if ( player.IsNPC() )
+ boltWidth = ARC_CANNON_BOLT_WIDTH_NPC
+ thread CreateArcCannonBeam( weapon, null, muzzleOrigin, beamEnd, player, ARC_CANNON_BEAM_LIFETIME, radius, boltWidth, 2, false, true )
+
+ #if SERVER
+ PlayImpactFXTable( expect vector( beamEnd ), player, ARC_CANNON_FX_TABLE, SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+ #endif
+}
+
+function FireArcWithTargets( entity weapon, table firstTargetInfo, WeaponPrimaryAttackParams attackParams, muzzleOrigin )
+{
+ local beamStart = muzzleOrigin
+ local beamEnd
+ entity player = weapon.GetWeaponOwner()
+ local chargeFrac = GetWeaponChargeFrac( weapon )
+ float radius = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_RADIUS_MIN, ARC_CANNON_BOLT_RADIUS_MAX )
+ float boltWidth = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_WIDTH_MIN, ARC_CANNON_BOLT_WIDTH_MAX )
+ local maxChains
+ local minChains
+
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ {
+ if ( player.IsNPC() )
+ maxChains = ARC_CANNON_CHAIN_COUNT_NPC_BURN
+ else
+ maxChains = ARC_CANNON_CHAIN_COUNT_MAX_BURN
+
+ minChains = ARC_CANNON_CHAIN_COUNT_MIN_BURN
+ }
+ else
+ {
+ if ( player.IsNPC() )
+ maxChains = ARC_CANNON_CHAIN_COUNT_NPC
+ else
+ maxChains = ARC_CANNON_CHAIN_COUNT_MAX
+
+ minChains = ARC_CANNON_CHAIN_COUNT_MIN
+ }
+
+ if ( !player.IsNPC() )
+ maxChains = Graph( chargeFrac, 0, 1, minChains, maxChains )
+
+ table zapInfo = {}
+ zapInfo.weapon <- weapon
+ zapInfo.player <- player
+ zapInfo.muzzleOrigin <- muzzleOrigin
+ zapInfo.radius <- radius
+ zapInfo.boltWidth <- boltWidth
+ zapInfo.maxChains <- maxChains
+ zapInfo.chargeFrac <- chargeFrac
+ zapInfo.zappedTargets <- {}
+ zapInfo.zappedTargets[ firstTargetInfo.target ] <- true
+ zapInfo.dmgSourceID <- weapon.GetDamageSourceID()
+ local chainNum = 1
+ thread ZapTargetRecursive( expect entity( firstTargetInfo.target), zapInfo, zapInfo.muzzleOrigin, expect vector( firstTargetInfo.hitLocation ), chainNum )
+}
+
+function ZapTargetRecursive( entity target, table zapInfo, beamStartPos, vector ornull firstTargetBeamEndPos = null, chainNum = 1 )
+{
+ if ( !IsValid( target ) )
+ return
+
+ if ( !IsValid( zapInfo.weapon ) )
+ return
+
+ Assert( target in zapInfo.zappedTargets )
+ if ( chainNum > zapInfo.maxChains )
+ return
+ vector beamEndPos
+ if ( firstTargetBeamEndPos == null )
+ beamEndPos = target.GetWorldSpaceCenter()
+ else
+ beamEndPos = expect vector( firstTargetBeamEndPos )
+
+ waitthread ZapTarget( zapInfo, target, beamStartPos, beamEndPos, chainNum )
+
+ // Get other nearby targets we can chain to
+ #if SERVER
+ if ( !IsValid( zapInfo.weapon ) )
+ return
+
+ var noArcing = expect entity( zapInfo.weapon ).GetWeaponInfoFileKeyField( "disable_arc" )
+
+ if ( noArcing != null && noArcing == 1 )
+ return // no chaining on new arc cannon
+
+ // NOTE: 'target' could be invalid at this point (no corpse)
+ array<entity> chainTargets = GetArcCannonChainTargets( beamEndPos, target, zapInfo )
+ foreach( entity chainTarget in chainTargets )
+ {
+ local newChainNum = chainNum
+ if ( chainTarget.GetClassName() != "rpg_missile" )
+ newChainNum++
+ zapInfo.zappedTargets[ chainTarget ] <- true
+ thread ZapTargetRecursive( chainTarget, zapInfo, beamEndPos, null, newChainNum )
+ }
+
+ if ( IsValid( zapInfo.player ) && zapInfo.player.IsPlayer() && zapInfo.zappedTargets.len() >= 5 )
+ {
+ #if HAS_STATS
+ if ( chainNum == 5 )
+ UpdatePlayerStat( expect entity( zapInfo.player ), "misc_stats", "arcCannonMultiKills", 1 )
+ #endif
+ }
+ #endif
+}
+
+function ZapTarget( zapInfo, target, beamStartPos, beamEndPos, chainNum = 1 )
+{
+ expect entity( target )
+ expect vector( beamStartPos )
+ expect vector( beamEndPos )
+
+ //DebugDrawLine( beamStartPos, beamEndPos, 255, 0, 0, true, 5.0 )
+ local boltWidth = zapInfo.boltWidth
+ if ( zapInfo.player.IsNPC() )
+ boltWidth = ARC_CANNON_BOLT_WIDTH_NPC
+ local firstBeam = ( chainNum == 1 )
+ #if SERVER
+ if ( firstBeam )
+ {
+ PlayImpactFXTable( beamEndPos, expect entity( zapInfo.player ), ARC_CANNON_FX_TABLE, SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+ }
+ #endif
+
+ thread CreateArcCannonBeam( zapInfo.weapon, target, beamStartPos, beamEndPos, zapInfo.player, ARC_CANNON_BEAM_LIFETIME, zapInfo.radius, boltWidth, 5, true, firstBeam )
+
+ #if SERVER
+ local isMissile = ( target.GetClassName() == "rpg_missile" )
+ if ( !isMissile )
+ wait ARC_CANNON_FORK_DELAY
+ else
+ wait 0.05
+
+ local deathPackage = damageTypes.arcCannon
+
+ float damageAmount
+ int damageMin
+ int damageMax
+
+ int damageFarValue = eWeaponVar.damage_far_value
+ int damageNearValue = eWeaponVar.damage_near_value
+ int damageFarValueTitanArmor = eWeaponVar.damage_far_value_titanarmor
+ int damageNearValueTitanArmor = eWeaponVar.damage_near_value_titanarmor
+ if ( zapInfo.player.IsNPC() )
+ {
+ damageFarValue = eWeaponVar.npc_damage_far_value
+ damageNearValue = eWeaponVar.npc_damage_near_value
+ damageFarValueTitanArmor = eWeaponVar.npc_damage_far_value_titanarmor
+ damageNearValueTitanArmor = eWeaponVar.npc_damage_near_value_titanarmor
+ }
+
+ if ( IsValid( target ) && IsValid( zapInfo.player ) )
+ {
+ bool hasFastPacitor = false
+ bool noArcing = false
+
+ if ( IsValid( zapInfo.weapon ) )
+ {
+ entity weap = expect entity( zapInfo.weapon )
+ hasFastPacitor = weap.GetWeaponInfoFileKeyField( "push_apart" ) != null && weap.GetWeaponInfoFileKeyField( "push_apart" ) == 1
+ noArcing = weap.GetWeaponInfoFileKeyField( "no_arcing" ) != null && weap.GetWeaponInfoFileKeyField( "no_arcing" ) == 1
+ }
+
+ if ( target.GetArmorType() == ARMOR_TYPE_HEAVY )
+ {
+ if ( IsValid( zapInfo.weapon ) )
+ {
+ entity weapon = expect entity( zapInfo.weapon )
+ damageMin = weapon.GetWeaponSettingInt( damageFarValueTitanArmor )
+ damageMax = weapon.GetWeaponSettingInt( damageNearValueTitanArmor )
+ }
+ else
+ {
+ damageMin = 100
+ damageMax = zapInfo.player.IsNPC() ? 1200 : 800
+ }
+ }
+ else
+ {
+ if ( IsValid( zapInfo.weapon ) )
+ {
+ entity weapon = expect entity( zapInfo.weapon )
+ damageMin = weapon.GetWeaponSettingInt( damageFarValue )
+ damageMax = weapon.GetWeaponSettingInt( damageNearValue )
+ }
+ else
+ {
+ damageMin = 120
+ damageMax = zapInfo.player.IsNPC() ? 140 : 275
+ }
+
+ if ( target.IsNPC() )
+ {
+ damageMin *= 3 // more powerful against NPC humans so they die easy
+ damageMax *= 3
+ }
+ }
+
+
+ local chargeRatio = GetArcCannonChargeFraction( zapInfo.weapon )
+ if ( IsValid( zapInfo.weapon ) && !zapInfo.weapon.GetWeaponSettingBool( eWeaponVar.charge_require_input ) )
+ {
+ // use distance for damage if the weapon auto-fires
+ entity weapon = expect entity( zapInfo.weapon )
+ float nearDist = weapon.GetWeaponSettingFloat( eWeaponVar.damage_near_distance )
+ float farDist = weapon.GetWeaponSettingFloat( eWeaponVar.damage_far_distance )
+
+ float dist = Distance( weapon.GetOrigin(), target.GetOrigin() )
+ damageAmount = GraphCapped( dist, farDist, nearDist, damageMin, damageMax )
+ }
+ else
+ {
+ // Scale damage amount based on how many chains deep we are
+ damageAmount = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, damageMin, damageMax )
+ }
+ local damageFalloff = ARC_CANNON_DAMAGE_FALLOFF_SCALER
+ if ( IsValid( zapInfo.weapon ) && zapInfo.weapon.HasMod( "splitter" ) )
+ damageFalloff = SPLITTER_DAMAGE_FALLOFF_SCALER
+ damageAmount *= pow( damageFalloff, chainNum - 1 )
+
+ local dmgSourceID = zapInfo.dmgSourceID
+
+ // Update Later - This shouldn't be done here, this is not where we determine if damage actually happened to the target
+ // move to Damaged callback instead
+ if ( damageAmount > 0 )
+ {
+ float empDuration = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, ARC_CANNON_EMP_DURATION_MIN, ARC_CANNON_EMP_DURATION_MAX )
+
+ if ( target.IsPlayer() && target.IsTitan() && !hasFastPacitor && !noArcing )
+ {
+ float empViewStrength = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, ARC_CANNON_SCREEN_EFFECTS_MIN, ARC_CANNON_SCREEN_EFFECTS_MAX )
+
+ if ( target.IsTitan() && zapInfo.chargeFrac >= ARC_CANNON_SCREEN_THRESHOLD )
+ {
+ Remote_CallFunction_Replay( target, "ServerCallback_TitanEMP", empViewStrength, empDuration, ARC_CANNON_EMP_FADEOUT_DURATION )
+ EmitSoundOnEntityOnlyToPlayer( target, target, ARC_CANNON_TITAN_SCREEN_SFX )
+ }
+ else if ( zapInfo.chargeFrac >= ARC_CANNON_SCREEN_THRESHOLD )
+ {
+ StatusEffect_AddTimed( target, eStatusEffect.emp, empViewStrength, empDuration, ARC_CANNON_EMP_FADEOUT_DURATION )
+ EmitSoundOnEntityOnlyToPlayer( target, target, ARC_CANNON_PILOT_SCREEN_SFX )
+ }
+ }
+
+ // Do 3rd person effect on the body
+ asset effect
+ string tag
+ target.TakeDamage( damageAmount, zapInfo.player, zapInfo.player, { origin = beamEndPos, force = Vector(0,0,0), scriptType = deathPackage, weapon = zapInfo.weapon, damageSourceId = dmgSourceID } )
+ //vector dir = Normalize( beamEndPos - beamStartPos )
+ //vector velocity = dir * 600
+ //PushPlayerAway( target, velocity )
+ //PushPlayerAway( expect entity( zapInfo.player ), -velocity )
+
+ if ( IsValid( zapInfo.weapon ) && hasFastPacitor )
+ {
+ if ( IsAlive( target ) && IsAlive( expect entity( zapInfo.player ) ) && target.IsTitan() )
+ {
+ float pushPercent = GraphCapped( damageAmount, damageMin, damageMax, 0.0, 1.0 )
+
+ if ( pushPercent > 0.6 )
+ PushPlayersApart( target, expect entity( zapInfo.player ), pushPercent * 400.0 )
+ }
+ }
+
+ if ( zapInfo.chargeFrac < ARC_CANNON_SCREEN_THRESHOLD )
+ empDuration = ARC_CANNON_3RD_PERSON_EFFECT_MIN_DURATION
+ else
+ empDuration += ARC_CANNON_EMP_FADEOUT_DURATION
+
+ if ( target.GetArmorType() == ARMOR_TYPE_HEAVY )
+ {
+ effect = $"impact_arc_cannon_titan"
+ tag = "exp_torso_front"
+ }
+ else
+ {
+ effect = $"P_emp_body_human"
+ tag = "CHESTFOCUS"
+ }
+
+ if ( target.IsPlayer() )
+ {
+ if ( target.LookupAttachment( tag ) != 0 )
+ ClientStylePlayFXOnEntity( effect, target, tag, empDuration )
+ }
+
+ if ( target.IsPlayer() )
+ EmitSoundOnEntityExceptToPlayer( target, target, "Titan_Blue_Electricity_Cloud" )
+ else
+ EmitSoundOnEntity( target, "Titan_Blue_Electricity_Cloud" )
+
+ thread FadeOutSoundOnEntityAfterDelay( target, "Titan_Blue_Electricity_Cloud", empDuration * 0.6666, empDuration * 0.3333 )
+ }
+ else
+ {
+ //Don't bounce if the beam is set to do 0 damage.
+ chainNum = zapInfo.maxChains
+ }
+
+ if ( isMissile )
+ {
+ if ( IsValid( zapInfo.player ) )
+ target.SetOwner( zapInfo.player )
+ target.MissileExplode()
+ }
+ }
+ #endif // SERVER
+}
+
+
+#if SERVER
+
+void function PushEntForTime( entity ent, vector velocity, float time )
+{
+ ent.EndSignal( "OnDeath" )
+ float endTime = Time() + time
+ float startTime = Time()
+ for ( ;; )
+ {
+ if ( Time() >= endTime )
+ break
+ float multiplier = Graph( Time(), startTime, endTime, 1.0, 0.0 )
+ vector currentVel = ent.GetVelocity()
+ currentVel += velocity * multiplier
+ ent.SetVelocity( currentVel )
+ WaitFrame()
+ }
+}
+
+array<entity> function GetArcCannonChainTargets( vector fromOrigin, entity fromTarget, table zapInfo )
+{
+ // NOTE: fromTarget could be null/invalid if it was a drone
+ array<entity> results = []
+ if ( !IsValid( zapInfo.player ) )
+ return results
+
+ int playerTeam = expect entity( zapInfo.player ).GetTeam()
+ array<entity> allTargets = GetArcCannonTargetsInRange( fromOrigin, playerTeam, expect entity( zapInfo.weapon ) )
+ allTargets = ArrayClosest( allTargets, fromOrigin )
+
+ local viewVector
+ if ( zapInfo.player.IsPlayer() )
+ viewVector = zapInfo.player.GetViewVector()
+ else
+ viewVector = AnglesToForward( zapInfo.player.EyeAngles() )
+
+ local eyePosition = zapInfo.player.EyePosition()
+
+ foreach ( ent in allTargets )
+ {
+ local forkCount = ARC_CANNON_FORK_COUNT_MAX
+ if ( zapInfo.weapon.HasMod( "splitter" ) )
+ forkCount = SPLITTER_FORK_COUNT_MAX
+ else if ( zapInfo.weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ forkCount = ARC_CANNON_FORK_COUNT_MAX_BURN
+
+ if ( results.len() >= forkCount )
+ break
+
+ if ( ent.IsPhaseShifted() )
+ continue
+
+ if ( ent.IsPlayer() )
+ {
+ // Ignore players that are passing damage to their parent. This is to address zapping a friendly rodeo player
+ local entParent = ent.GetParent()
+ if ( IsValid( entParent ) && ent.kv.PassDamageToParent.tointeger() )
+ continue
+
+ // only chains to other titan players for now
+ if ( !ent.IsTitan() )
+ continue
+ }
+
+ if ( ent.GetClassName() == "script_mover" )
+ continue
+
+ if ( IsEntANeutralMegaTurret( ent, playerTeam ) )
+ continue
+
+ if ( !IsAlive( ent ) )
+ continue
+
+ // Don't consider targets that already got zapped
+ if ( ent in zapInfo.zappedTargets )
+ continue
+
+ //Preventing the arc-cannon from firing behind.
+ local vecToEnt = ( ent.GetWorldSpaceCenter() - eyePosition )
+ vecToEnt.Norm()
+ local dotVal = DotProduct( vecToEnt, viewVector )
+ if ( dotVal < 0 )
+ continue
+
+ // Check if we can see them, they aren't behind a wall or something
+ local ignoreEnts = []
+ ignoreEnts.append( zapInfo.player )
+ ignoreEnts.append( ent )
+
+ foreach( zappedTarget, val in zapInfo.zappedTargets )
+ {
+ if ( IsValid( zappedTarget ) )
+ ignoreEnts.append( zappedTarget )
+ }
+
+ TraceResults traceResult = TraceLineHighDetail( fromOrigin, ent.GetWorldSpaceCenter(), ignoreEnts, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+
+ // Trace failed, lets try an eye to eye trace
+ if ( traceResult.fraction < 1 )
+ {
+ // 'fromTarget' may be invalid
+ if ( IsValid( fromTarget ) )
+ traceResult = TraceLineHighDetail( fromTarget.EyePosition(), ent.EyePosition(), ignoreEnts, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+ }
+
+ if ( traceResult.fraction < 1 )
+ continue
+
+ // Enemy is in visible, and within range.
+ if ( !results.contains( ent ) )
+ results.append( ent )
+ }
+
+ //printt( "NEARBY TARGETS VALID AND VISIBLE:", results.len() )
+
+ return results
+}
+#endif // SERVER
+
+bool function IsEntANeutralMegaTurret( ent, int playerTeam )
+{
+ expect entity( ent )
+
+ if ( ent.GetClassName() != "npc_turret_mega" )
+ return false
+ int entTeam = ent.GetTeam()
+ if ( entTeam == playerTeam )
+ return false
+ if ( !IsEnemyTeam( playerTeam, entTeam ) )
+ return true
+
+ return false
+}
+
+function ArcCannon_HideIdleEffect( entity weapon, delay )
+{
+ bool weaponOwnerIsPilot = IsPilot( weapon.GetWeaponOwner() )
+ weapon.EndSignal( ARC_CANNON_SIGNAL_DEACTIVATED )
+ if ( weaponOwnerIsPilot == false )
+ {
+ weapon.StopWeaponEffect( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity" )
+ weapon.StopWeaponSound( "arc_cannon_charged_loop" )
+ }
+ wait delay
+
+ if ( !IsValid( weapon ) )
+ return
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ //The weapon can be valid, but the player isn't a Titan during melee execute.
+ // JFS: threads with waits should just end on "OnDestroy"
+ if ( !IsValid( weaponOwner ) )
+ return
+
+ if ( weapon != weaponOwner.GetActiveWeapon() )
+ return
+
+ if ( weaponOwnerIsPilot == false )
+ {
+ weapon.PlayWeaponEffectNoCull( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity", "muzzle_flash" )
+ weapon.EmitWeaponSound( "arc_cannon_charged_loop" )
+ }
+ else
+ {
+ weapon.EmitWeaponSound_1p3p( "Arc_Rifle_charged_Loop_1P", "Arc_Rifle_charged_Loop_3P" )
+ }
+}
+
+#if SERVER
+void function AddToArcCannonTargets( entity ent )
+{
+ AddToScriptManagedEntArray( level._arcCannonTargetsArrayID, ent );
+}
+
+function RemoveArcCannonTarget( ent )
+{
+ RemoveFromScriptManagedEntArray( level._arcCannonTargetsArrayID, ent )
+}
+
+array<entity> function GetArcCannonTargets( vector origin, int team )
+{
+ array<entity> targets = GetScriptManagedEntArrayWithinCenter( level._arcCannonTargetsArrayID, team, origin, ARC_CANNON_TITAN_RANGE_CHAIN )
+
+ if ( ARC_CANNON_TARGETS_MISSILES )
+ targets.extend( GetProjectileArrayEx( "rpg_missile", TEAM_ANY, team, origin, ARC_CANNON_TITAN_RANGE_CHAIN ) )
+
+ return targets
+}
+
+array<entity> function GetArcCannonTargetsInRange( vector origin, int team, entity weapon )
+{
+ array<entity> allTargets = GetArcCannonTargets( origin, team )
+ array<entity> targetsInRange
+
+ float titanDistSq
+ float distSq
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ {
+ titanDistSq = ARC_CANNON_TITAN_RANGE_CHAIN_BURN * ARC_CANNON_TITAN_RANGE_CHAIN_BURN
+ distSq = ARC_CANNON_RANGE_CHAIN_BURN * ARC_CANNON_RANGE_CHAIN_BURN
+ }
+ else
+ {
+ titanDistSq = ARC_CANNON_TITAN_RANGE_CHAIN * ARC_CANNON_TITAN_RANGE_CHAIN
+ distSq = ARC_CANNON_RANGE_CHAIN * ARC_CANNON_RANGE_CHAIN
+ }
+
+ foreach( target in allTargets )
+ {
+ float d = DistanceSqr( target.GetOrigin(), origin )
+ float validDist = target.IsTitan() ? titanDistSq : distSq
+ if ( d <= validDist )
+ targetsInRange.append( target )
+ }
+
+ return targetsInRange
+}
+#endif // SERVER
+
+function CreateArcCannonBeam( weapon, target, startPos, endPos, player, lifeDuration = ARC_CANNON_BEAM_LIFETIME, radius = 256, boltWidth = 4, noiseAmplitude = 5, hasTarget = true, firstBeam = false )
+{
+ Assert( startPos )
+ Assert( endPos )
+
+ //**************************
+ // LIGHTNING BEAM EFFECT
+ //**************************
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ lifeDuration = ARC_CANNON_BEAM_LIFETIME_BURN
+ // If it's the first beam and on client we do a special beam so it's lined up with the muzzle origin
+ #if CLIENT
+ if ( firstBeam )
+ thread CreateClientArcBeam( weapon, endPos, lifeDuration, target )
+ #endif
+
+ #if SERVER
+ // Control point sets the end position of the effect
+ entity cpEnd = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpEnd, UniqueString( "arc_cannon_beam_cpEnd" ) )
+ cpEnd.SetOrigin( endPos )
+ DispatchSpawn( cpEnd )
+
+ entity zapBeam = CreateEntity( "info_particle_system" )
+ zapBeam.kv.cpoint1 = cpEnd.GetTargetName()
+
+ zapBeam.SetValueForEffectNameKey( GetBeamEffect( weapon ) )
+
+ zapBeam.kv.start_active = 0
+ zapBeam.SetOwner( player )
+ zapBeam.SetOrigin( startPos )
+ if ( firstBeam )
+ {
+ zapBeam.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // everyone but owner
+ zapBeam.SetParent( player.GetActiveWeapon(), "muzzle_flash", false, 0.0 )
+ }
+ DispatchSpawn( zapBeam )
+
+ zapBeam.Fire( "Start" )
+ zapBeam.Fire( "StopPlayEndCap", "", lifeDuration )
+ zapBeam.Kill_Deprecated_UseDestroyInstead( lifeDuration )
+ cpEnd.Kill_Deprecated_UseDestroyInstead( lifeDuration )
+ #endif
+}
+
+function GetBeamEffect( weapon )
+{
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ return ARC_CANNON_BEAM_EFFECT_MOD
+
+ return ARC_CANNON_BEAM_EFFECT
+}
+
+#if CLIENT
+function CreateClientArcBeam( weapon, endPos, lifeDuration, target )
+{
+ Assert( IsClient() )
+
+ local beamEffect = GetBeamEffect( weapon )
+
+ // HACK HACK HACK HACK
+ string tag = "muzzle_flash"
+ if ( weapon.GetWeaponInfoFileKeyField( "client_tag_override" ) != null )
+ tag = expect string( weapon.GetWeaponInfoFileKeyField( "client_tag_override" ) )
+
+ local handle = weapon.PlayWeaponEffectReturnViewEffectHandle( beamEffect, $"", tag )
+ if ( !EffectDoesExist( handle ) )
+ return
+
+ EffectSetControlPointVector( handle, 1, endPos )
+
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ lifeDuration = ARC_CANNON_BEAM_LIFETIME_BURN
+
+ wait( lifeDuration )
+
+ if ( IsValid( weapon ) )
+ weapon.StopWeaponEffect( beamEffect, $"" )
+}
+
+void function ClientDestroyCallback_ArcCannon_Stop( entity ent )
+{
+ ArcCannon_Stop( ent )
+}
+#endif // CLIENT
+
+function GetArcCannonChargeFraction( weapon )
+{
+ if ( IsValid( weapon ) )
+ {
+ local chargeRatio = ARC_CANNON_DAMAGE_CHARGE_RATIO
+ if ( weapon.HasMod( "capacitor" ) )
+ chargeRatio = ARC_CANNON_CAPACITOR_CHARGE_RATIO
+ if ( weapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ chargeRatio = ARC_CANNON_DAMAGE_CHARGE_RATIO_BURN
+ return chargeRatio
+ }
+
+ return 0
+}
+
+function GetWeaponChargeFrac( weapon )
+{
+ if ( weapon.IsChargeWeapon() )
+ return weapon.GetWeaponChargeFraction()
+ return 1.0
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_at_turrets.gnut b/Northstar.CustomServers/scripts/vscripts/weapons/_at_turrets.gnut
new file mode 100644
index 00000000..b061c182
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_at_turrets.gnut
@@ -0,0 +1,284 @@
+untyped
+
+global function ATTurrets_Init
+global function CreateATTurret
+global function ATTurretSettings
+//global function SetDriverOnTurret
+global function PROTO_ATTurretsEnabled
+global function PROTO_Simulate_Turret_Use
+
+const USE_DEBOUNCE_TIME = 0.3
+const FX_ANTI_TITAN_SHIELD_WALL = $"P_anti_titan_shield_3P"
+const vector AT_TURRET_SHIELD_COLOR = Vector( 115, 247, 255 )
+
+void function ATTurrets_Init()
+{
+ AddSpawnCallbackEditorClass( "turret", "turret_pilot_at", ATTurretSettings )
+ RegisterSignal( "ClearDriver" )
+ RegisterSignal( "DismebarkATTurret" )
+}
+
+void function CreateATTurret( vector origin, vector angles )
+{
+ entity turret = CreateEntity( "turret" )
+ turret.kv.editorclass = "turret_pilot_at"
+ turret.kv.settings = "PROTO_at_turret"
+ turret.kv.teamnumber = 0
+ turret.SetValueForModelKey( $"models/weapons/sentry_turret/sentry_turret.mdl" )
+ turret.kv.origin = origin
+ turret.kv.angles = angles
+ DispatchSpawn( turret )
+ ATTurretSettings( turret )
+}
+
+void function ATTurretSettings( entity turret )
+{
+ if ( PROTO_ATTurretsEnabled() )
+ {
+ turret.SetUsable()
+ turret.SetUsableByGroup( "pilot" )
+ turret.SetUsePrompts( "Hold %use% to use AT-Turret", "Press %use% to use AT-Turret" )
+ //AddCallback_OnUseEntity( turret, SetDriverOnTurret )
+ AddCallback_OnUseEntity( turret, PROTO_Simulate_Turret_Use )
+
+ local attachmentID = turret.LookupAttachment( "muzzle_flash" )
+ local origin = turret.GetAttachmentOrigin( attachmentID )
+ local angles = turret.GetAttachmentAngles( attachmentID )
+
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ turret.e.shieldWallFX = CreateEntity( "info_particle_system" )
+ entity shieldWallFX = turret.e.shieldWallFX
+ shieldWallFX.SetValueForEffectNameKey( FX_ANTI_TITAN_SHIELD_WALL )
+ shieldWallFX.kv.start_active = 1
+ SetShieldWallCPoint( shieldWallFX, cpoint )
+ shieldWallFX.SetOwner( turret )
+ shieldWallFX.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // not owner only
+ shieldWallFX.kv.cpoint1 = cpoint.GetTargetName()
+ shieldWallFX.SetStopType( "destroyImmediately" )
+ shieldWallFX.SetOrigin( origin )
+ shieldWallFX.SetAngles( angles - Vector(0,0,90) )
+ shieldWallFX.SetParent( turret, "muzzle_flash", true, 0.0 )
+ DispatchSpawn( shieldWallFX )
+ SetShieldWallCPointOrigin( shieldWallFX, AT_TURRET_SHIELD_COLOR )
+ }
+ else
+ {
+ turret.DisableDraw()
+ turret.NotSolid()
+ }
+}
+
+bool function PROTO_ATTurretsEnabled()
+{
+ return ( GetCurrentPlaylistVarInt( "at_turrets_enabled", 0 ) == 1 )
+}
+
+/*/////////////////////////////////////////////////////////////////
+ WEAPON PROTOTYPE
+///////////////////////////////////////////////////////////////////*/
+
+function PROTO_Simulate_Turret_Use( turret, player )
+{
+ expect entity( turret )
+ expect entity( player )
+
+ if ( Time() < player.p.PROTO_UseDebounceEndTime )
+ return
+
+ PROTO_ActivateTurret( turret, player )
+}
+
+const array<int> TURRET_CANCEL_BUTTONS =
+[
+ IN_USE,
+ IN_DUCK,
+ IN_DUCKTOGGLE,
+ IN_WEAPON_CYCLE,
+ IN_MELEE,
+ IN_OFFHAND0,
+ IN_OFFHAND1,
+ IN_OFFHAND2,
+ IN_OFFHAND3,
+ IN_OFFHAND4,
+ IN_JUMP
+]
+
+void function PROTO_ActivateTurret( entity turret, entity player )
+{
+ if ( turret.GetOwner() == player )
+ {
+ player.Signal( "DismebarkATTurret" )
+ }
+ else
+ {
+ if ( turret.GetOwner() == null )
+ {
+ turret.DisableDraw()
+ turret.NotSolid()
+ SetShieldWallCPointOrigin( turret.e.shieldWallFX, < 0, 0, 0 > )
+ turret.SetOwner( player )
+ player.p.PROTO_UseDebounceEndTime = Time() + USE_DEBOUNCE_TIME
+ foreach( int button in TURRET_CANCEL_BUTTONS )
+ AddButtonPressedPlayerInputCallback( player, button, PROTO_DisembarkATTurret )
+ AddEntityCallback_OnDamaged( player, PlayerDamagedWhileOnTurret )
+ thread MonitorPilot( turret, player )
+ }
+ else
+ {
+ SendHudMessage( player, "Turret in use.", -1, 0.4, 255, 255, 0, 255, 0.0, 0.5, 0.0 )
+ }
+ }
+}
+
+void function PlayerDamagedWhileOnTurret( entity player, var damageInfo )
+{
+ if ( Time() < player.p.PROTO_UseDebounceEndTime )
+ return
+
+ player.Signal( "DismebarkATTurret" )
+}
+
+function MonitorPilot( entity turret, entity player )
+{
+ player.EndSignal( "OnDestroy" )
+ player.EndSignal( "DismebarkATTurret")
+ turret.EndSignal( "OnDestroy" )
+
+ player.ForceStand()
+ entity playerMover = CreateOwnedScriptMover( player )
+ player.SetParent( playerMover, "ref", true )
+ vector forward = turret.GetForwardVector()
+ vector basePos = turret.GetOrigin() + forward * -25
+ vector startOrigin = player.GetOrigin()
+ float moveTime = 0.1
+ playerMover.NonPhysicsMoveTo( basePos, moveTime, 0.0, 0.0 )
+ playerMover.NonPhysicsRotateTo( turret.GetAngles(), moveTime, 0, 0 )
+ player.FreezeControlsOnServer()
+
+ StorePilotWeapons( player )
+
+ OnThreadEnd(
+ function() : ( turret, player, playerMover, startOrigin )
+ {
+ if ( IsValid( player ) )
+ {
+ player.ClearParent()
+ player.UnforceStand()
+ ClearPlayerAnimViewEntity( player )
+ player.UnfreezeControlsOnServer()
+ RetrievePilotWeapons( player )
+ ViewConeZeroInstant( player )
+ foreach( int button in TURRET_CANCEL_BUTTONS )
+ RemoveButtonPressedPlayerInputCallback( player, button, PROTO_DisembarkATTurret )
+ RemoveEntityCallback_OnDamaged( player, PlayerDamagedWhileOnTurret )
+ player.p.PROTO_UseDebounceEndTime = Time() + USE_DEBOUNCE_TIME
+ PutEntityInSafeSpot( player, turret, null, startOrigin, player.GetOrigin() )
+ }
+
+ if ( IsValid( turret ) )
+ {
+ turret.EnableDraw()
+ turret.Solid()
+ SetShieldWallCPointOrigin( turret.e.shieldWallFX, AT_TURRET_SHIELD_COLOR )
+ turret.SetOwner( null )
+ }
+
+ playerMover.Destroy()
+ }
+ )
+
+ wait moveTime
+
+ player.PlayerCone_SetSpecific( forward )
+ ViewConeZeroInstant( player )
+
+ // PROTO: Supporting ability to pick different turret weapons for turrets in LevelEd and the legacy Defender prototype turret
+ // We need a predator cannon style turret in SP.
+ if ( IsMultiplayer() )
+ {
+ //player.GiveWeapon( "mp_weapon_smr", [ "PROTO_at_turret" ] )
+ //player.SetActiveWeaponByName( "mp_weapon_smr" )
+
+ // modded code: smr's at turret mod does exist in release tf2
+ player.GiveWeapon( "mp_weapon_defender", [ "PROTO_at_turret" ] )
+ player.SetActiveWeaponByName( "mp_weapon_defender" )
+ }
+ else if ( turret.HasKey( "weaponsettings" ) )
+ {
+ // See if we have any special turret mods on this weapon
+ array<string> turretMods = []
+ array<string> mods = GetWeaponMods_Global( turret.kv.weaponsettings )
+ foreach ( mod in mods )
+ {
+ if ( mod.find( "PROTO_at_turret" ) == 0 )
+ turretMods.append( "PROTO_at_turret" )
+ }
+
+ player.GiveWeapon( turret.kv.weaponsettings, turretMods )
+ player.SetActiveWeaponByName( turret.kv.weaponsettings )
+ }
+
+ wait 0.1
+
+ player.UnfreezeControlsOnServer()
+
+ ViewConeLockedForward( player )
+
+ player.WaitSignal( "OnDeath" )
+}
+
+void function PROTO_DisembarkATTurret( entity player )
+{
+ if ( Time() < player.p.PROTO_UseDebounceEndTime )
+ return
+
+ player.Signal( "DismebarkATTurret" )
+}
+
+/*/////////////////////////////////////////////////////////////////
+ TURRET ENTITY PROTOTYPE
+///////////////////////////////////////////////////////////////////*/
+//
+//function SetDriverOnTurret( turret, player )
+//{
+// if ( turret.GetOwner() == player )
+// {
+// turret.SetOwner( null )
+// turret.ClearDriver()
+// player.Signal( "ClearDriver" )
+// }
+// else
+// {
+// entity oldOwner = expect entity( turret.GetOwner() )
+// if ( oldOwner != null )
+// {
+// oldOwner.Signal( "ClearDriver" )
+// turret.ClearDriver()
+// }
+// turret.SetOwner( player )
+// turret.SetDriver( player )
+// thread ClearDriverOnDeath( turret, player )
+// }
+// //turret.SetUsePrompts( "DEACTIVATE", "DEACTIVATE" )
+// //turret.SetOwner( player )
+// //turret.SetBossPlayer( player )
+// //turret.SetUsableByGroup( "owner pilot" )
+//}
+//
+//function ClearDriverOnDeath( turret, player )
+//{
+// player.EndSignal( "ClearDriver" )
+// player.EndSignal( "OnDestroy" )
+// turret.EndSignal( "OnDestroy" )
+//
+// player.WaitSignal( "OnDeath" )
+//
+// if ( IsValid( turret ) )
+// turret.ClearDriver()
+//}
+////TODO: Handle death and handle deactivate.
+//
+// \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_ball_lightning.gnut b/Northstar.CustomServers/scripts/vscripts/weapons/_ball_lightning.gnut
new file mode 100644
index 00000000..9aae59e5
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_ball_lightning.gnut
@@ -0,0 +1,363 @@
+untyped
+
+global function BallLightning_Init
+global function AttachBallLightning
+global function AttachBallLightningToProp
+global function CreateBallLightning
+global function DestroyBallLightningOnEnt
+global function GetBallLightningFromEnt
+
+global function RegisterBallLightningDamage
+
+global function BallLightningZapFX
+global function BallLightningZapTargets
+global function BallLightningZapConnectionFX
+
+struct {
+ table< string, float > uniqueStrings
+} file
+
+function BallLightning_Init()
+{
+ PrecacheParticleSystem( BALL_LIGHTNING_ZAP_FX )
+
+ if ( BALL_LIGHTNING_FX_TABLE != "" )
+ PrecacheImpactEffectTable( BALL_LIGHTNING_FX_TABLE )
+
+ RegisterBallLightningDamage( eDamageSourceId.mp_weapon_arc_launcher )
+ RegisterBallLightningDamage( eDamageSourceId.mp_titanweapon_arc_ball )
+ RegisterBallLightningDamage( eDamageSourceId.mp_weapon_arc_trap )
+}
+
+function AttachBallLightning( entity weapon, entity projectile )
+{
+ Assert( !( "ballLightning" in projectile.s ) )
+
+ int damageSourceId
+ entity owner
+
+ if ( weapon.IsProjectile() )
+ {
+ owner = weapon.GetOwner()
+ damageSourceId = weapon.ProjectileGetDamageSourceID()
+ }
+ else
+ {
+ owner = weapon.GetWeaponOwner()
+ damageSourceId = weapon.GetDamageSourceID()
+ }
+
+
+ entity ball = CreateBallLightning( owner, damageSourceId, projectile.GetOrigin(), projectile.GetAngles() )
+ ball.SetParent( projectile )
+ projectile.s.ballLightning <- ball
+}
+
+void function DestroyBallLightningOnEnt( entity prop )
+{
+ if ( "ballLightning" in prop.s )
+ {
+ prop.s.ballLightning.Destroy()
+ delete prop.s.ballLightning
+ }
+}
+
+
+entity function AttachBallLightningToProp( entity prop, entity owner, int damageSourceId )
+{
+ entity ball = CreateBallLightning( owner, damageSourceId, prop.GetOrigin(), prop.GetAngles() )
+ ball.SetParent( prop )
+ prop.s.ballLightning <- ball
+ return ball
+}
+
+entity function CreateBallLightning( entity owner, int damageSourceId, vector origin, vector angles )
+{
+ entity ballLightning = CreateScriptMover( origin, angles )
+ ballLightning.SetOwner( owner )
+ SetTeam( ballLightning, owner.GetTeam() )
+
+ thread BallLightningThink( ballLightning, damageSourceId )
+ return ballLightning
+}
+
+void function RegisterBallLightningDamage( int damageSourceId )
+{
+ AddDamageCallbackSourceID( damageSourceId, OnBallLightningDamage )
+}
+
+void function OnBallLightningDamage( entity victim, var damageInfo )
+{
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ if ( damage <= 0 )
+ return
+
+ if ( victim.IsWorld() )
+ return
+
+ if ( victim.IsProjectile() )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & (DF_EXPLOSION | DF_IMPACT) )
+ return
+
+ // if ( IsHumanSized( victim ) )
+ // {
+ // DamageInfo_SetDamage( damageInfo, 0 )
+ // return
+ // }
+
+ entity ballLightning = DamageInfo_GetInflictor( damageInfo )
+
+ if ( victim == ballLightning )
+ return
+
+ if ( victim.GetParent() == ballLightning )
+ return
+
+ if ( !IsTargetEntValid( ballLightning, victim, ballLightning.e.ballLightningData ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ vector origin = DamageInfo_GetDamagePosition( damageInfo )
+ int hitBox = DamageInfo_GetHitBox( damageInfo )
+
+ string tag = GetEntityCenterTag( victim )
+ thread BallLightningZapConnectionFX( ballLightning, victim, tag, ballLightning.e.ballLightningData )
+ BallLightningZapFX( ballLightning, victim, tag, ballLightning.e.ballLightningData )
+}
+
+void function BallLightningThink( entity ballLightning, int damageSourceId )
+{
+ ballLightning.EndSignal( "OnDestroy" )
+
+ EmitSoundOnEntity( ballLightning, "Weapon_Arc_Ball_Loop" )
+
+ local data = {}
+
+ OnThreadEnd(
+ function() : ( ballLightning, data )
+ {
+ if ( IsValid( ballLightning ) )
+ StopSoundOnEntity( ballLightning, "Weapon_Arc_Ball_Loop" )
+ }
+ )
+
+ int inflictorTeam = ballLightning.GetTeam()
+ ballLightning.e.ballLightningTargetsIdx = CreateScriptManagedEntArray()
+
+ WaitEndFrame()
+
+ while( 1 )
+ {
+ for( int i=0; i<BALL_LIGHTNING_BURST_NUM; i++ )
+ {
+ if ( BALL_LIGHTNING_BURST_NUM > 1 )
+ wait BALL_LIGHTNING_BURST_PAUSE
+
+ vector origin = ballLightning.GetOrigin()
+ BallLightningZapTargets( ballLightning, origin, inflictorTeam, damageSourceId, ballLightning.e.ballLightningData, false )
+ }
+ wait BALL_LIGHTNING_BURST_DELAY
+ }
+}
+
+void function BallLightningZapTargets( entity ballLightning, vector origin, int inflictorTeam, int damageSourceId, BallLightningData fxData, bool single )
+{
+ RadiusDamage(
+ origin, // origin
+ ballLightning.GetOwner(), // owner
+ ballLightning, // inflictor
+ fxData.damageToPilots, // normal damage
+ fxData.damage, // heavy armor damage
+ fxData.radius, // inner radius
+ fxData.radius, // outer radius
+ SF_ENVEXPLOSION_NO_DAMAGEOWNER, // explosion flags
+ 0, // distanceFromAttacker
+ 0, // explosionForce
+ fxData.deathPackage, // damage flags
+ damageSourceId // damage source id
+ )
+}
+
+string function GetEntityCenterTag( entity target )
+{
+ string tag = "center"
+
+ if ( IsHumanSized( target ) )
+ tag = "CHESTFOCUS"
+ else if ( target.IsTitan() )
+ tag = "HIJACK"
+ else if ( IsSuperSpectre( target ) || IsAirDrone( target ) )
+ tag = "CHESTFOCUS"
+ else if ( IsDropship( target ) )
+ tag = "ORIGIN"
+ else if ( target.GetClassName() == "npc_turret_mega" )
+ tag = "ATTACH"
+
+ return tag
+}
+
+bool function IsTargetEntValid( entity ballLightning, entity target, BallLightningData fxData )
+{
+ if ( !IsValid( target ) )
+ return false
+
+ vector origin = ballLightning.GetOrigin()
+
+ if ( target == ballLightning )
+ return false
+
+ if ( target == ballLightning.GetParent() )
+ return false
+
+ if ( target.GetParent() == ballLightning.GetParent() )
+ return false
+
+ // if ( target.IsPlayer() && !target.IsTitan() )
+ // return false
+
+ if ( fabs( origin.z - target.GetOrigin().z ) > fxData.height )
+ return false
+
+ if ( GetBugReproNum() != 131703 )
+ {
+ if ( target.GetModelName() == $"" )
+ return false
+ }
+
+ if ( !( target.GetClassName() in ArcCannonTargetClassnames ) )
+ return false
+
+ vector entityCenter = target.GetWorldSpaceCenter()
+
+ if ( target.GetModelName() != $"" )
+ {
+ string tag = GetEntityCenterTag( target )
+ int index = target.LookupAttachment( tag )
+
+ if ( index == 0 )
+ return false
+
+ entityCenter = target.GetAttachmentOrigin( index )
+ }
+
+ vector fwd = AnglesToForward( ballLightning.GetAngles() )
+ vector fwdToEnemy = Normalize( entityCenter - ballLightning.GetOrigin() )
+
+ float dot = DotProduct( fwd, fwdToEnemy )
+
+ if ( dot < fxData.minDot )
+ return false
+
+
+ if ( IsHumanSized( target ) )
+ {
+ float maxDist = fxData.humanRadius
+ if ( Distance( entityCenter, ballLightning.GetOrigin() ) > maxDist )
+ return false
+ }
+
+ // array<entity> ignoreEnts = [ target, ballLightning ]
+ // if ( ballLightning.GetParent() != null )
+ // ignoreEnts.append( ballLightning.GetParent() )
+
+ // TraceResults trace = TraceLine( ballLightning.GetOrigin(), entityCenter, ignoreEnts, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_BLOCK_WEAPONS )
+
+ // if ( trace.fraction < 1 )
+ // return false
+
+ VortexBulletHit ornull vortexHit = VortexBulletHitCheck( ballLightning.GetOwner(), ballLightning.GetOrigin(), entityCenter )
+
+ if ( vortexHit )
+ return false
+
+ return true
+}
+
+void function BallLightningZapConnectionFX( entity ballLightning, entity target, string tag, BallLightningData fxData )
+{
+ if ( fxData.zapFx != $"" )
+ {
+ // Control point sets the end position of the effect
+ entity cpEnd = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpEnd, GetUniqueCpString() )
+ cpEnd.SetParent( target, tag, false, 0.0 )
+ DispatchSpawn( cpEnd )
+
+ entity zapBeam = CreateEntity( "info_particle_system" )
+ zapBeam.kv.cpoint1 = cpEnd.GetTargetName()
+
+ zapBeam.SetValueForEffectNameKey( fxData.zapFx )
+ zapBeam.kv.start_active = 0
+ zapBeam.SetOwner( ballLightning )
+ zapBeam.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ zapBeam.SetParent( ballLightning, "", false, 0.0 )
+ DispatchSpawn( zapBeam )
+
+ zapBeam.Fire( "Start" )
+
+ OnThreadEnd(
+ function() : ( zapBeam, cpEnd )
+ {
+ if ( IsValid( zapBeam ) )
+ zapBeam.Destroy()
+ if ( IsValid( cpEnd ) )
+ cpEnd.Destroy()
+ }
+ )
+
+ ballLightning.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDestroy" )
+ target.EndSignal( "OnDeath" )
+
+ if ( fxData.zapLifetime > 0 )
+ {
+ wait fxData.zapLifetime
+ }
+ }
+}
+
+void function BallLightningZapFX( entity ballLightning, entity target, string tag, BallLightningData fxData )
+{
+ int index = target.LookupAttachment( tag )
+
+ vector entityCenter = target.GetAttachmentOrigin( index )
+
+ if ( fxData.zapImpactTable != "" )
+ PlayImpactFXTable( entityCenter, ballLightning.GetOwner(), fxData.zapImpactTable, SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+
+ EmitSoundOnEntity( ballLightning, fxData.zapSound )
+ thread FadeOutSoundOnEntityAfterDelay( ballLightning, fxData.zapSound, 0.2, 0.2 )
+}
+
+// This is to minimize creation of new Unique Strings
+string function GetUniqueCpString()
+{
+ foreach ( string uString, float useTime in file.uniqueStrings )
+ {
+ if ( useTime + BALL_LIGHTNING_ZAP_LIFETIME*2 > Time() )
+ continue
+
+ file.uniqueStrings[ uString ] = Time()
+ return uString
+ }
+
+ string newString = UniqueString( "ball_lightning_cpEnd" )
+
+ // printt( "Generated new string " + newString )
+
+ file.uniqueStrings[ newString ] <- Time()
+ return newString
+}
+
+entity function GetBallLightningFromEnt( entity ent )
+{
+ if ( "ballLightning" in ent.s )
+ return expect entity( ent.s.ballLightning )
+
+ return null
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_cloaker.gnut b/Northstar.CustomServers/scripts/vscripts/weapons/_cloaker.gnut
new file mode 100644
index 00000000..6ec0bc0a
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_cloaker.gnut
@@ -0,0 +1,121 @@
+untyped
+
+global function CloakerThink
+global function CloakerShouldCloakGuy
+
+global function CloakerCloaksGuy
+global function CloakerDeCloaksGuy
+
+function CloakerThink( entity cloaker, float radius, array<string> ents = [ "any" ], vector offset = Vector(0,0,0), var shouldCloakGuyFunc = null, float waitTime = 0.25 )
+{
+ OnThreadEnd(
+ function() : ( cloaker )
+ {
+ local cloakList = clone cloaker.s.cloakList
+ foreach ( entity guy, value in cloakList )
+ {
+ if ( !IsAlive( guy ) )
+ continue
+
+ CloakerDeCloaksGuy( guy )
+ }
+ }
+ )
+
+ cloaker.s.cloakList <- {}
+ cloaker.s.decloakList <- {}
+
+ while( 1 )
+ {
+ vector origin = cloaker.GetOrigin() + offset
+ array<entity> guys
+
+ foreach ( entType in ents )
+ {
+ switch ( entType )
+ {
+ case "player":
+ case "players":
+ guys.extend( GetPlayerArrayEx( "any", cloaker.GetTeam(), TEAM_ANY, origin, radius ) )
+ break;
+ default:
+ guys.extend( GetNPCArrayEx( entType, cloaker.GetTeam(), TEAM_ANY, origin, radius ) )
+ break
+ }
+ }
+ int index = 0
+
+ float startTime = Time()
+
+ table cloakList = expect table( cloaker.s.cloakList )
+ cloaker.s.decloakList = clone cloakList
+
+ foreach ( guy in guys )
+ {
+ //only do 5 distanceSqr / cansee checks per frame
+ if ( index++ > 5 )
+ {
+ wait 0.1
+ index = 0
+ origin = cloaker.GetOrigin() + offset
+ }
+
+ bool shouldCloakGuy = CloakerShouldCloakGuy( cloaker, guy )
+
+ if ( shouldCloakGuy )
+ shouldCloakGuy = expect bool( shouldCloakGuyFunc( cloaker, guy ) )
+
+ if ( shouldCloakGuy )
+ {
+ if ( guy in cloaker.s.decloakList )
+ delete cloaker.s.decloakList[ guy ]
+
+ if ( IsCloaked( guy ) )
+ continue
+
+ cloakList[ guy ] <- true
+ CloakerCloaksGuy( guy )
+ }
+ }
+
+ foreach ( entity guy, value in cloaker.s.decloakList )
+ {
+ // any guys still in the decloakList shouldn't be decloaked ... if alive.
+ Assert( guy in cloakList )
+ delete cloakList[ guy ]
+
+ if ( IsAlive( guy ) )
+ CloakerDeCloaksGuy( guy )
+ }
+
+ float endTime = Time()
+ float elapsedTime = endTime - startTime
+ if ( elapsedTime < waitTime )
+ wait waitTime - elapsedTime
+ }
+}
+
+void function CloakerCloaksGuy( guy )
+{
+ guy.SetCloakDuration( 2.0, -1, 0 )
+ EmitSoundOnEntity( guy, CLOAKED_DRONE_CLOAK_START_SFX )
+ EmitSoundOnEntity( guy, CLOAKED_DRONE_CLOAK_LOOP_SFX )
+ guy.Minimap_Hide( TEAM_IMC, null )
+ guy.Minimap_Hide( TEAM_MILITIA, null )
+}
+
+void function CloakerDeCloaksGuy( guy )
+{
+ guy.SetCloakDuration( 0, 0, 1.5 )
+ StopSoundOnEntity( guy, CLOAKED_DRONE_CLOAK_LOOP_SFX )
+ guy.Minimap_AlwaysShow( TEAM_IMC, null )
+ guy.Minimap_AlwaysShow( TEAM_MILITIA, null )
+}
+
+bool function CloakerShouldCloakGuy( entity cloaker, entity guy )
+{
+ if ( !IsAlive( guy ) )
+ return false
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_grenade.nut b/Northstar.CustomServers/scripts/vscripts/weapons/_grenade.nut
new file mode 100644
index 00000000..c2036e85
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_grenade.nut
@@ -0,0 +1,604 @@
+untyped
+
+global function Grenade_FileInit
+global function GetGrenadeThrowSound_1p
+global function GetGrenadeDeploySound_1p
+global function GetGrenadeThrowSound_3p
+global function GetGrenadeDeploySound_3p
+global function GetGrenadeProjectileSound
+
+const DEFAULT_FUSE_TIME = 2.25
+const DEFAULT_WARNING_TIME = 1.0
+global const float DEFAULT_MAX_COOK_TIME = 99999.9 //Longer than an entire day. Really just an arbitrarily large number
+
+global function Grenade_OnWeaponTossReleaseAnimEvent
+global function Grenade_OnWeaponTossCancelDrop
+global function Grenade_OnWeaponDeactivate
+global function Grenade_OnWeaponTossPrep
+global function Grenade_OnProjectileIgnite
+
+#if SERVER
+ global function Grenade_OnPlayerNPCTossGrenade_Common
+ global function ProxMine_Triggered
+ global function EnableTrapWarningSound
+ global function AddToProximityTargets
+ global function ProximityMineThink
+#endif
+global function Grenade_Init
+
+const GRENADE_EXPLOSIVE_WARNING_SFX_LOOP = "Weapon_Vortex_Gun.ExplosiveWarningBeep"
+const EMP_MAGNETIC_FORCE = 1600
+const MAG_FLIGHT_SFX_LOOP = "Explo_MGL_MagneticAttract"
+
+//Proximity Mine Settings
+global const PROXIMITY_MINE_EXPLOSION_DELAY = 1.2
+global const PROXIMITY_MINE_ARMING_DELAY = 1.0
+const TRIGGERED_ALARM_SFX = "Weapon_ProximityMine_CloseWarning"
+global const THERMITE_GRENADE_FX = $"P_grenade_thermite"
+global const CLUSTER_BASE_FX = $"P_wpn_meteor_exp"
+
+global const ProximityTargetClassnames = {
+ [ "npc_soldier_shield" ] = true,
+ [ "npc_soldier_heavy" ] = true,
+ [ "npc_soldier" ] = true,
+ [ "npc_spectre" ] = true,
+ [ "npc_drone" ] = true,
+ [ "npc_titan" ] = true,
+ [ "npc_marvin" ] = true,
+ [ "player" ] = true,
+ [ "npc_turret_mega" ] = true,
+ [ "npc_turret_sentry" ] = true,
+ [ "npc_dropship" ] = true,
+}
+
+const SOLDIER_ARC_STUN_ANIMS = [
+ "pt_react_ARC_fall",
+ "pt_react_ARC_kneefall",
+ "pt_react_ARC_sidefall",
+ "pt_react_ARC_slowfall",
+ "pt_react_ARC_scream",
+ "pt_react_ARC_stumble_F",
+ "pt_react_ARC_stumble_R" ]
+
+function Grenade_FileInit()
+{
+ PrecacheParticleSystem( CLUSTER_BASE_FX )
+
+ RegisterSignal( "ThrowGrenade" )
+ RegisterSignal( "WeaponDeactivateEvent" )
+ RegisterSignal( "OnEMPPilotHit" )
+ RegisterSignal( "StopGrenadeClientEffects" )
+ RegisterSignal( "DisableTrapWarningSound" )
+
+ //Globalize( MagneticFlight )
+
+ #if CLIENT
+ AddDestroyCallback( "grenade_frag", ClientDestroyCallback_GrenadeDestroyed )
+ #endif
+
+ #if SERVER
+ level._empForcedCallbacks <- {}
+ level._proximityTargetArrayID <- CreateScriptManagedEntArray()
+
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_proximity_mine, ProxMine_Triggered )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_thermite_grenade, Thermite_DamagedPlayerOrNPC )
+ AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_frag_grenade, Frag_DamagedPlayerOrNPC )
+
+ level._empForcedCallbacks[eDamageSourceId.mp_weapon_grenade_emp] <- true
+ level._empForcedCallbacks[eDamageSourceId.mp_weapon_proximity_mine] <- true
+
+ PrecacheParticleSystem( THERMITE_GRENADE_FX )
+ #endif
+}
+
+void function Grenade_OnWeaponTossPrep( entity weapon, WeaponTossPrepParams prepParams )
+{
+ weapon.w.startChargeTime = Time()
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ weapon.EmitWeaponSound_1p3p( GetGrenadeDeploySound_1p( weapon ), GetGrenadeDeploySound_3p( weapon ) )
+
+ #if SERVER
+ thread HACK_CookGrenade( weapon, weaponOwner )
+ thread HACK_DropGrenadeOnDeath( weapon, weaponOwner )
+ #elseif CLIENT
+ if ( weaponOwner.IsPlayer() )
+ {
+ weaponOwner.p.grenadePulloutTime = Time()
+ }
+ #endif
+}
+
+void function Grenade_OnWeaponDeactivate( entity weapon )
+{
+ StopSoundOnEntity( weapon, GRENADE_EXPLOSIVE_WARNING_SFX_LOOP )
+ weapon.Signal( "WeaponDeactivateEvent" )
+}
+
+void function Grenade_OnProjectileIgnite( entity weapon )
+{
+ printt( "Grenade_OnProjectileIgnite() callback." )
+}
+
+function Grenade_Init( entity grenade, entity weapon )
+{
+ entity weaponOwner = weapon.GetOwner()
+ if ( IsValid( weaponOwner ) )
+ SetTeam( grenade, weaponOwner.GetTeam() )
+
+ // JFS: this is because I don't know if the above line should be
+ // weapon.GetOwner() or it's a typo and should really be weapon.GetWeaponOwner()
+ // and it's too close to ship and who knows what effect that will have
+ entity owner = weapon.GetWeaponOwner()
+ if ( IsMultiplayer() && IsValid( owner ) )
+ {
+ if ( owner.IsNPC() )
+ {
+ SetTeam( grenade, owner.GetTeam() )
+ }
+ }
+
+ #if SERVER
+ bool smartPistolVisible = weapon.GetWeaponSettingBool( eWeaponVar.projectile_visible_to_smart_ammo )
+ if ( smartPistolVisible )
+ {
+ grenade.SetDamageNotifications( true )
+ grenade.SetTakeDamageType( DAMAGE_EVENTS_ONLY )
+ grenade.proj.onlyAllowSmartPistolDamage = true
+
+ if ( !grenade.GetProjectileWeaponSettingBool( eWeaponVar.projectile_damages_owner ) && !grenade.GetProjectileWeaponSettingBool( eWeaponVar.explosion_damages_owner ) )
+ SetCustomSmartAmmoTarget( grenade, true ) // prevent friendly target lockon
+ }
+ else
+ {
+ grenade.SetTakeDamageType( DAMAGE_NO )
+ }
+ #endif
+ if ( IsValid( weaponOwner ) )
+ grenade.s.originalOwner <- weaponOwner // for later in damage callbacks, to skip damage vs friendlies but not for og owner or his enemies
+}
+
+
+int function Grenade_OnWeaponToss_( entity weapon, WeaponPrimaryAttackParams attackParams, float directionScale )
+{
+ weapon.EmitWeaponSound_1p3p( GetGrenadeThrowSound_1p( weapon ), GetGrenadeThrowSound_3p( weapon ) )
+ bool projectilePredicted = PROJECTILE_PREDICTED
+ bool projectileLagCompensated = PROJECTILE_LAG_COMPENSATED
+#if SERVER
+ if ( weapon.IsForceReleaseFromServer() )
+ {
+ projectilePredicted = false
+ projectileLagCompensated = false
+ }
+#endif
+ entity grenade = Grenade_Launch( weapon, attackParams.pos, (attackParams.dir * directionScale), projectilePredicted, projectileLagCompensated )
+ entity weaponOwner = weapon.GetWeaponOwner()
+ weaponOwner.Signal( "ThrowGrenade" )
+
+ PlayerUsedOffhand( weaponOwner, weapon ) // intentionally here and in Hack_DropGrenadeOnDeath - accurate for when cooldown actually begins
+
+#if SERVER
+
+ #if BATTLECHATTER_ENABLED
+ TryPlayWeaponBattleChatterLine( weaponOwner, weapon )
+ #endif
+
+#endif
+
+ return weapon.GetWeaponSettingInt( eWeaponVar.ammo_per_shot )
+}
+
+var function Grenade_OnWeaponTossReleaseAnimEvent( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ var result = Grenade_OnWeaponToss_( weapon, attackParams, 1.0 )
+ return result
+}
+
+var function Grenade_OnWeaponTossCancelDrop( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ var result = Grenade_OnWeaponToss_( weapon, attackParams, 0.2 )
+ return result
+}
+
+// Can return entity or nothing
+entity function Grenade_Launch( entity weapon, vector attackPos, vector throwVelocity, bool isPredicted, bool isLagCompensated )
+{
+ #if CLIENT
+ if ( !weapon.ShouldPredictProjectiles() )
+ return null
+ #endif
+
+ //TEMP FIX while Deploy anim is added to sprint
+ float currentTime = Time()
+ if ( weapon.w.startChargeTime == 0.0 )
+ weapon.w.startChargeTime = currentTime
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+
+ var discThrow = weapon.GetWeaponInfoFileKeyField( "grenade_disc_throw" )
+
+ vector angularVelocity = Vector( 3600, RandomFloatRange( -1200, 1200 ), 0 )
+
+ if ( discThrow == 1 )
+ angularVelocity = Vector( 100, 100, RandomFloatRange( 1200, 2200 ) )
+
+
+ float fuseTime
+
+ float baseFuseTime = weapon.GetGrenadeFuseTime() //Note that fuse time of 0 means the grenade won't explode on its own, instead it depends on OnProjectileCollision() functions to be defined and explode there. Arguably in this case grenade_fuse_time shouldn't be 0, but an arbitrarily large number instead.
+ if ( baseFuseTime > 0.0 )
+ {
+ fuseTime = baseFuseTime - ( currentTime - weapon.w.startChargeTime )
+ if ( fuseTime <= 0 )
+ fuseTime = 0.001
+ }
+ else
+ {
+ fuseTime = baseFuseTime
+ }
+
+ int damageFlags = weapon.GetWeaponDamageFlags()
+ entity frag = weapon.FireWeaponGrenade( attackPos, throwVelocity, angularVelocity, fuseTime, damageFlags, damageFlags, isPredicted, isLagCompensated, true )
+ if ( frag == null )
+ return null
+
+ if ( discThrow == 1 ) // add wobble by pitching it slightly
+ {
+ Assert( !frag.IsMarkedForDeletion(), "Frag before .SetAngles() is marked for deletion." )
+ frag.SetAngles( frag.GetAngles() + < RandomFloatRange( 7,11 ),0,0 > )
+ //Assert( !frag.IsMarkedForDeletion(), "Frag after .SetAngles() is marked for deletion." )
+ if ( frag.IsMarkedForDeletion() )
+ {
+ CodeWarning( "Frag after .SetAngles() was marked for deletion." )
+ return null
+ }
+ }
+
+ Grenade_OnPlayerNPCTossGrenade_Common( weapon, frag )
+
+ return frag
+}
+
+void function Grenade_OnPlayerNPCTossGrenade_Common( entity weapon, entity frag )
+{
+ Grenade_Init( frag, weapon )
+ #if SERVER
+ thread TrapExplodeOnDamage( frag, 20, 0.0, 0.0 )
+
+ string projectileSound = GetGrenadeProjectileSound( weapon )
+ if ( projectileSound != "" )
+ EmitSoundOnEntity( frag, projectileSound )
+ #endif
+
+ if( weapon.HasMod( "burn_mod_emp_grenade" ) )
+ frag.InitMagnetic( EMP_MAGNETIC_FORCE, MAG_FLIGHT_SFX_LOOP )
+}
+
+struct CookGrenadeStruct //Really just a convenience struct so we can read the changed value of a bool in an OnThreadEnd
+{
+ bool shouldOverrideFuseTime = false
+}
+
+void function HACK_CookGrenade( entity weapon, entity weaponOwner )
+{
+ float maxCookTime = GetMaxCookTime( weapon )
+ if ( maxCookTime >= DEFAULT_MAX_COOK_TIME )
+ return
+
+ weaponOwner.EndSignal( "OnDeath" )
+ weaponOwner.EndSignal( "ThrowGrenade" )
+ weapon.EndSignal( "WeaponDeactivateEvent" )
+ weapon.EndSignal( "OnDestroy" )
+
+ /*CookGrenadeStruct grenadeStruct
+
+ OnThreadEnd(
+ function() : ( weapon, grenadeStruct )
+ {
+ if ( grenadeStruct.shouldOverrideFuseTime )
+ {
+ var minFuseTime = weapon.GetWeaponInfoFileKeyField( "min_fuse_time" )
+ printt( "minFuseTime: " + minFuseTime )
+ if ( minFuseTime != null )
+ {
+ expect float( minFuseTime )
+ printt( "Setting overrideFuseTime to : " + weapon.GetWeaponInfoFileKeyField( "min_fuse_time" ) )
+ weapon.w.overrideFuseTime = minFuseTime
+ }
+ }
+ }
+ )
+*/
+ if ( maxCookTime - DEFAULT_WARNING_TIME <= 0 )
+ {
+ EmitSoundOnEntity( weapon, GRENADE_EXPLOSIVE_WARNING_SFX_LOOP )
+ wait maxCookTime
+ }
+ else
+ {
+ wait( maxCookTime - DEFAULT_WARNING_TIME )
+
+ EmitSoundOnEntity( weapon, GRENADE_EXPLOSIVE_WARNING_SFX_LOOP )
+
+ wait( DEFAULT_WARNING_TIME )
+ }
+
+ if ( !IsValid( weapon.GetWeaponOwner() ) )
+ return
+
+ weapon.ForceReleaseFromServer() // Will eventually result in Grenade_OnWeaponToss_() or equivalent function
+
+ // JFS: prevent grenade cook exploit in coliseum
+ if ( GameRules_GetGameMode() == COLISEUM )
+ {
+ #if SERVER
+ int damageSource = weapon.GetDamageSourceID()
+
+ if ( damageSource == eDamageSourceId.mp_weapon_frag_grenade )
+ {
+ var impact_effect_table = weapon.GetWeaponInfoFileKeyField( "impact_effect_table" )
+ if ( impact_effect_table != null )
+ {
+ string fx = expect string( impact_effect_table )
+ PlayImpactFXTable( weaponOwner.EyePosition(), weaponOwner, fx )
+ }
+ weaponOwner.Die( weaponOwner, weapon, { damageSourceId = damageSource } )
+ }
+ #endif
+ }
+
+ weaponOwner.Signal( "ThrowGrenade" ) // Only necessary to end HACK_DropGrenadeOnDeath
+}
+
+
+void function HACK_WaitForGrenadeDropEvent( weapon, entity weaponOwner )
+{
+ weapon.EndSignal( "WeaponDeactivateEvent" )
+
+ weaponOwner.WaitSignal( "OnDeath" )
+}
+
+
+void function HACK_DropGrenadeOnDeath( entity weapon, entity weaponOwner )
+{
+ if ( weapon.HasMod( "burn_card_weapon_mod" ) ) //JFS: Primarily to stop boost grenade weapons (e.g. frag_drone ) not doing TryUsingBurnCardWeapon() when dropped through this function.
+ return
+
+ weaponOwner.EndSignal( "ThrowGrenade" )
+ weaponOwner.EndSignal( "OnDestroy" )
+
+ waitthread HACK_WaitForGrenadeDropEvent( weapon, weaponOwner )
+
+ if( !IsValid( weaponOwner ) || !IsValid( weapon ) || IsAlive( weaponOwner ) )
+ return
+
+ float elapsedTime = Time() - weapon.w.startChargeTime
+ float baseFuseTime = weapon.GetGrenadeFuseTime()
+ float fuseDelta = (baseFuseTime - elapsedTime)
+
+ if ( (baseFuseTime == 0.0) || (fuseDelta > -0.1) )
+ {
+ float forwardScale = weapon.GetWeaponSettingFloat( eWeaponVar.grenade_death_drop_velocity_scale )
+ vector velocity = weaponOwner.GetForwardVector() * forwardScale
+ velocity.z += weapon.GetWeaponSettingFloat( eWeaponVar.grenade_death_drop_velocity_extraUp )
+ vector angularVelocity = Vector( 0, 0, 0 )
+ float fuseTime = baseFuseTime ? baseFuseTime - elapsedTime : baseFuseTime
+
+ int primaryClipCount = weapon.GetWeaponPrimaryClipCount()
+ int ammoPerShot = weapon.GetWeaponSettingInt( eWeaponVar.ammo_per_shot )
+ weapon.SetWeaponPrimaryClipCountAbsolute( maxint( 0, primaryClipCount - ammoPerShot ) )
+
+ PlayerUsedOffhand( weaponOwner, weapon ) // intentionally here and in ReleaseAnimEvent - for cases where grenade is dropped on death
+
+ entity grenade = Grenade_Launch( weapon, weaponOwner.GetOrigin(), velocity, PROJECTILE_NOT_PREDICTED, PROJECTILE_NOT_LAG_COMPENSATED )
+ }
+}
+
+
+#if SERVER
+void function ProxMine_Triggered( entity ent, var damageInfo )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( !IsValid( attacker ) )
+ return
+
+ if ( attacker == ent )
+ return
+
+ if ( ent.IsPlayer() || ent.IsNPC() )
+ thread ShowProxMineTriggeredIcon( ent )
+
+ //If this feature is good, we should add this to NPCs as well. Currently script errors if applied to an NPC.
+ //if ( ent.IsPlayer() )
+ // thread ProxMine_ShowOnMinimapTimed( ent, GetOtherTeam( ent.GetTeam() ), PROX_MINE_MARKER_TIME )
+}
+
+/*
+function ProxMine_ShowOnMinimapTimed( ent, teamToDisplayEntTo, duration )
+{
+ ent.Minimap_AlwaysShow( teamToDisplayEntTo, null )
+ Minimap_CreatePingForTeam( teamToDisplayEntTo, ent.GetOrigin(), $"vgui/HUD/titanFiringPing", 1.0 )
+
+ wait duration
+
+ if ( IsValid( ent ) && ent.IsPlayer() )
+ ent.Minimap_DisplayDefault( teamToDisplayEntTo, ent )
+}
+*/
+
+void function Thermite_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ if ( !IsValid( ent ) )
+ return
+
+ Thermite_DamagePlayerOrNPCSounds( ent )
+}
+
+void function Frag_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ #if MP
+ if ( !IsValid( ent ) || ent.IsPlayer() || ent.IsTitan() )
+ return
+
+ if ( ent.IsMechanical() )
+ DamageInfo_ScaleDamage( damageInfo, 0.5 )
+ #endif
+}
+
+#endif // SERVER
+
+
+#if CLIENT
+void function ClientDestroyCallback_GrenadeDestroyed( entity grenade )
+{
+}
+#endif // CLIENT
+
+#if SERVER
+function EnableTrapWarningSound( entity trap, delay = 0, warningSound = DEFAULT_WARNING_SFX )
+{
+ trap.EndSignal( "OnDestroy" )
+ trap.EndSignal( "DisableTrapWarningSound" )
+
+ if ( delay > 0 )
+ wait delay
+
+ while ( IsValid( trap ) )
+ {
+ EmitSoundOnEntity( trap, warningSound )
+ wait 1.0
+ }
+}
+
+void function AddToProximityTargets( entity ent )
+{
+ AddToScriptManagedEntArray( level._proximityTargetArrayID, ent );
+}
+
+function ProximityMineThink( entity proximityMine, entity owner )
+{
+ proximityMine.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( proximityMine )
+ {
+ if ( IsValid( proximityMine ) )
+ proximityMine.Destroy()
+ }
+ )
+ thread TrapExplodeOnDamage( proximityMine, 50 )
+
+ wait PROXIMITY_MINE_ARMING_DELAY
+
+ int teamNum = proximityMine.GetTeam()
+ float explodeRadius = proximityMine.GetDamageRadius()
+ float triggerRadius = ( ( explodeRadius * 0.75 ) + 0.5 )
+ local lastTimeNPCsChecked = 0
+ local NPCTickRate = 0.5
+ local PlayerTickRate = 0.2
+
+ // Wait for someone to enter proximity
+ while( IsValid( proximityMine ) && IsValid( owner ) )
+ {
+ if ( lastTimeNPCsChecked + NPCTickRate <= Time() )
+ {
+ array<entity> nearbyNPCs = GetNPCArrayEx( "any", TEAM_ANY, teamNum, proximityMine.GetOrigin(), triggerRadius )
+ foreach( ent in nearbyNPCs )
+ {
+ if ( ShouldSetOffProximityMine( proximityMine, ent ) )
+ {
+ ProximityMine_Explode( proximityMine )
+ return
+ }
+ }
+ lastTimeNPCsChecked = Time()
+ }
+
+ array<entity> nearbyPlayers = GetPlayerArrayEx( "any", TEAM_ANY, teamNum, proximityMine.GetOrigin(), triggerRadius )
+ foreach( ent in nearbyPlayers )
+ {
+ if ( ShouldSetOffProximityMine( proximityMine, ent ) )
+ {
+ ProximityMine_Explode( proximityMine )
+ return
+ }
+ }
+
+ wait PlayerTickRate
+ }
+}
+
+function ProximityMine_Explode( proximityMine )
+{
+ local explodeTime = Time() + PROXIMITY_MINE_EXPLOSION_DELAY
+ EmitSoundOnEntity( proximityMine, TRIGGERED_ALARM_SFX )
+
+ wait PROXIMITY_MINE_EXPLOSION_DELAY
+
+ if ( IsValid( proximityMine ) )
+ proximityMine.GrenadeExplode( proximityMine.GetForwardVector() )
+}
+
+bool function ShouldSetOffProximityMine( entity proximityMine, entity ent )
+{
+ if ( !IsAlive( ent ) )
+ return false
+
+ if ( ent.IsPhaseShifted() )
+ return false
+
+ TraceResults results = TraceLine( proximityMine.GetOrigin(), ent.EyePosition(), proximityMine, (TRACE_MASK_SHOT | CONTENTS_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+ if ( results.fraction >= 1 || results.hitEnt == ent )
+ return true
+
+ return false
+}
+
+#endif // SERVER
+
+
+
+float function GetMaxCookTime( entity weapon )
+{
+ var cookTime = weapon.GetWeaponInfoFileKeyField( "max_cook_time" )
+ if (cookTime == null )
+ return DEFAULT_MAX_COOK_TIME
+
+ expect float ( cookTime )
+ return cookTime
+}
+
+function GetGrenadeThrowSound_1p( weapon )
+{
+ return weapon.GetWeaponInfoFileKeyField( "sound_throw_1p" ) ? weapon.GetWeaponInfoFileKeyField( "sound_throw_1p" ) : ""
+}
+
+
+function GetGrenadeDeploySound_1p( weapon )
+{
+ return weapon.GetWeaponInfoFileKeyField( "sound_deploy_1p" ) ? weapon.GetWeaponInfoFileKeyField( "sound_deploy_1p" ) : ""
+}
+
+
+function GetGrenadeThrowSound_3p( weapon )
+{
+ return weapon.GetWeaponInfoFileKeyField( "sound_throw_3p" ) ? weapon.GetWeaponInfoFileKeyField( "sound_throw_3p" ) : ""
+}
+
+
+function GetGrenadeDeploySound_3p( weapon )
+{
+ return weapon.GetWeaponInfoFileKeyField( "sound_deploy_3p" ) ? weapon.GetWeaponInfoFileKeyField( "sound_deploy_3p" ) : ""
+}
+
+string function GetGrenadeProjectileSound( weapon )
+{
+ return expect string( weapon.GetWeaponInfoFileKeyField( "sound_grenade_projectile" ) ? weapon.GetWeaponInfoFileKeyField( "sound_grenade_projectile" ) : "" )
+}
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_particle_wall.gnut b/Northstar.CustomServers/scripts/vscripts/weapons/_particle_wall.gnut
new file mode 100644
index 00000000..a46bfff8
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_particle_wall.gnut
@@ -0,0 +1,460 @@
+untyped
+
+global function ParticleWall_Init
+
+global function CreateTurretParticleWall
+global function CreateParticleWallFromOwner
+global function CreateShieldWithSettings
+global function DrainHealthOverTime
+
+global function CreateAmpedWallFromOwner
+
+global function CreateParticleWallForOwnerFromDirection
+
+global const SHIELD_WALL_COL_MODEL = $"models/fx/xo_shield_wall.mdl"
+global const SHIELD_WALL_FX = $"P_xo_shield_wall"
+
+global const TURRET_SHIELD_WALL_COL_MODEL = $"models/fx/turret_shield_wall.mdl"
+global const TURRET_SHIELD_WALL_FX = $"P_turret_shield_wall"
+
+global const AMPED_WALL_FX = $"P_xo_amped_wall"
+#if MP
+global const SHIELD_WALL_HEALTH = 2000
+global const TURRET_SHIELD_WALL_HEALTH = 3500//1750
+#else
+global const SHIELD_WALL_HEALTH = 1750
+global const TURRET_SHIELD_WALL_HEALTH = 1750
+#endif
+global const PAS_TONE_WALL_HEALTH = 3000
+global const PAS_TONE_WALL_DURATION_MULTIPLIER = 1.5
+global const SHIELD_WALL_DURATION = 8.0
+global const SHIELD_WALL_RADIUS = 180
+global const SHIELD_WALL_FOV = 120
+global const SHIELD_WALL_WIDTH = 156.0 // SHIELD_WALL_RADIUS * cos( SHIELD_WALL_FOV/2 )
+
+global function UpdateShieldWallColorForFrac
+global function PlayEffectOnVortexSphere
+global function SetVortexSphereShieldWallCPoint
+global function SetShieldWallCPoint
+global function StopShieldWallFX
+global function StopShieldWallFXOverTime
+global function SetShieldWallCPointOrigin
+
+function ParticleWall_Init()
+{
+ PrecacheParticleSystem( SHIELD_WALL_FX )
+ PrecacheModel( SHIELD_WALL_COL_MODEL )
+
+ PrecacheParticleSystem( TURRET_SHIELD_WALL_FX )
+ PrecacheModel( TURRET_SHIELD_WALL_COL_MODEL )
+
+ PrecacheParticleSystem( AMPED_WALL_FX )
+}
+
+void function CreateParticleWallFromOwner( entity weaponOwner, float duration, WeaponPrimaryAttackParams attackParams )
+{
+ vector dir = GetParticleWallAttackAnglesFromOwner( weaponOwner, attackParams )
+ CreateParticleWallForOwnerFromDirection( weaponOwner, duration, dir )
+}
+
+vector function GetParticleWallAttackAnglesFromOwner( entity weaponOwner, WeaponPrimaryAttackParams attackParams )
+{
+ if ( weaponOwner.IsNPC() )
+ return attackParams.dir
+
+ vector angles = weaponOwner.CameraAngles()
+ angles.x = 0
+ return AnglesToForward( angles )
+}
+
+void function CreateParticleWallForOwnerFromDirection( entity weaponOwner, float duration, vector dir )
+{
+ Assert( IsServer() )
+
+ entity titanSoul = weaponOwner.GetTitanSoul()
+
+ // JFS the weapon owner should always have a soul, at least on the server
+ if ( !IsValid( titanSoul ) )
+ return
+
+ vector origin = weaponOwner.GetOrigin()
+ vector safeSpot = origin
+ vector angles = VectorToAngles( dir )
+
+ if ( weaponOwner.IsNPC() )
+ {
+ // spawn in front of npc a bit
+ origin += dir * 100
+ }
+
+ float endTime = Time() + duration
+ titanSoul.SetDefensivePlacement( endTime, SHIELD_WALL_WIDTH, 0, true, safeSpot, dir )
+
+ Assert( weaponOwner.IsTitan() )
+ Assert( titanSoul )
+
+ int health
+ if ( SoulHasPassive( titanSoul, ePassives.PAS_TONE_WALL ) )
+ {
+ health = PAS_TONE_WALL_HEALTH
+ duration *= PAS_TONE_WALL_DURATION_MULTIPLIER
+ }
+ else
+ {
+ health = SHIELD_WALL_HEALTH
+ }
+ entity vortexSphere = CreateShieldWithSettings( origin + < 0, 0, -64 >, angles, SHIELD_WALL_RADIUS, SHIELD_WALL_RADIUS * 2, SHIELD_WALL_FOV, duration, health, SHIELD_WALL_FX )
+ thread DrainHealthOverTime( vortexSphere, vortexSphere.e.shieldWallFX, duration )
+
+ entity groundEntity = weaponOwner.GetGroundEntity()
+ if ( groundEntity != null && groundEntity.HasPusherRootParent() )
+ vortexSphere.SetParent( groundEntity, "", true, 0 )
+}
+
+entity function CreateTurretParticleWall( vector origin, vector angles, float duration )
+{
+ Assert( IsServer() )
+
+ entity vortexSphere = CreateTurretShieldWithSettings( origin + < 0, 0, -64 >, angles, SHIELD_WALL_RADIUS, int( SHIELD_WALL_RADIUS * 1.65 ), 270, duration, TURRET_SHIELD_WALL_HEALTH, TURRET_SHIELD_WALL_FX )
+ thread DrainHealthOverTime( vortexSphere, vortexSphere.e.shieldWallFX, duration )
+
+ return vortexSphere
+}
+
+entity function CreateShieldWithSettings( vector origin, vector angles, int radius, int height, int fov, float duration, int health, asset effectName )
+{
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+
+ vortexSphere.kv.spawnflags = SF_ABSORB_BULLETS | SF_BLOCK_OWNER_WEAPON | SF_BLOCK_NPC_WEAPON_LOF | SF_ABSORB_CYLINDER
+ vortexSphere.kv.enabled = 0
+ vortexSphere.kv.radius = radius
+ vortexSphere.kv.height = height
+ vortexSphere.kv.bullet_fov = fov
+ vortexSphere.kv.physics_pull_strength = 25
+ vortexSphere.kv.physics_side_dampening = 6
+ vortexSphere.kv.physics_fov = 360
+ vortexSphere.kv.physics_max_mass = 2
+ vortexSphere.kv.physics_max_size = 6
+
+ vortexSphere.SetAngles( angles ) // viewvec?
+ vortexSphere.SetOrigin( origin )
+ vortexSphere.SetMaxHealth( health )
+ vortexSphere.SetHealth( health )
+ vortexSphere.SetTakeDamageType( DAMAGE_YES )
+
+ DispatchSpawn( vortexSphere )
+
+ vortexSphere.Fire( "Enable" )
+ vortexSphere.Fire( "Kill", "", duration )
+
+ // Shield wall fx control point
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ // Shield wall fx
+ entity shieldWallFX = PlayFXWithControlPoint( effectName, origin, cpoint, -1, null, angles, C_PLAYFX_LOOP )
+ vortexSphere.e.shieldWallFX = shieldWallFX
+ shieldWallFX.SetParent( vortexSphere )
+ SetVortexSphereShieldWallCPoint( vortexSphere, cpoint )
+ StopShieldWallFXOverTime( vortexSphere, duration )
+
+
+ thread StopFXOnDestroy( vortexSphere, shieldWallFX, duration )
+ return vortexSphere
+}
+
+//Turret Shields do not block npc line of fire.
+entity function CreateTurretShieldWithSettings( vector origin, vector angles, int radius, int height, int fov, float duration, int health, asset effectName )
+{
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+
+ vortexSphere.kv.spawnflags = SF_ABSORB_BULLETS | SF_BLOCK_OWNER_WEAPON | SF_ABSORB_CYLINDER
+ vortexSphere.kv.enabled = 0
+ vortexSphere.kv.radius = radius
+ vortexSphere.kv.height = height
+ vortexSphere.kv.bullet_fov = fov
+ vortexSphere.kv.physics_pull_strength = 25
+ vortexSphere.kv.physics_side_dampening = 6
+ vortexSphere.kv.physics_fov = 360
+ vortexSphere.kv.physics_max_mass = 2
+ vortexSphere.kv.physics_max_size = 6
+
+ vortexSphere.SetAngles( angles ) // viewvec?
+ vortexSphere.SetOrigin( origin )
+ vortexSphere.SetMaxHealth( health )
+ vortexSphere.SetHealth( health )
+ vortexSphere.SetTakeDamageType( DAMAGE_YES )
+
+ DispatchSpawn( vortexSphere )
+
+ vortexSphere.Fire( "Enable" )
+ vortexSphere.Fire( "Kill", "", duration )
+
+ // Shield wall fx control point
+ entity cpoint = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpoint, UniqueString( "shield_wall_controlpoint" ) )
+ DispatchSpawn( cpoint )
+
+ // Shield wall fx
+ entity shieldWallFX = PlayFXWithControlPoint( effectName, origin, cpoint, -1, null, angles, C_PLAYFX_LOOP )
+ vortexSphere.e.shieldWallFX = shieldWallFX
+ shieldWallFX.SetParent( vortexSphere )
+ SetVortexSphereShieldWallCPoint( vortexSphere, cpoint )
+ StopShieldWallFXOverTime( vortexSphere, duration )
+
+
+ thread StopFXOnDestroy( vortexSphere, shieldWallFX, duration )
+ return vortexSphere
+}
+
+function StopFXOnDestroy( entity vortexSphere, entity shieldWallFX, float duration )
+{
+ vortexSphere.EndSignal( "OnDestroy" )
+ shieldWallFX.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( vortexSphere )
+ {
+ StopShieldWallFX( vortexSphere )
+ }
+ )
+
+ wait duration * 1.5
+}
+
+void function CreateAmpedWallFromOwner( entity weaponOwner, float duration, WeaponPrimaryAttackParams attackParams )
+{
+ Assert( IsNewThread(), "Must be threaded off" )
+ Assert( IsServer() )
+ entity titanSoul = weaponOwner.GetTitanSoul()
+
+ // JFS the weapon owner should always have a soul, at least on the server
+ if ( !IsValid( titanSoul ) )
+ return
+ Assert( weaponOwner.IsTitan() )
+
+ vector dir = GetParticleWallAttackAnglesFromOwner( weaponOwner, attackParams )
+ vector origin = weaponOwner.GetOrigin()
+ vector safeSpot = origin
+ vector angles = VectorToAngles( dir )
+ vector forward = AnglesToForward( angles )
+ angles = AnglesCompose( angles, <0,180,0> )
+
+ if ( weaponOwner.IsNPC() )
+ {
+ // spawn in front of npc a bit
+ origin += dir * 100
+ }
+
+ origin += dir * 500
+ origin += Vector(0,0,-64)
+
+ float endTime = Time() + duration
+ titanSoul.SetDefensivePlacement( endTime, SHIELD_WALL_WIDTH, 0, true, safeSpot, dir )
+
+ entity vortexSphere = CreateShieldWithSettings( origin, angles, SHIELD_WALL_RADIUS, SHIELD_WALL_RADIUS * 2, SHIELD_WALL_FOV, duration, SHIELD_WALL_HEALTH, AMPED_WALL_FX )
+ vortexSphere.EndSignal( "OnDestroy" )
+ entity shieldWallFX = vortexSphere.e.shieldWallFX
+ shieldWallFX.EndSignal( "OnDestroy" )
+ SetTargetName( vortexSphere, PROTO_AMPED_WALL ) // so projectiles pass through
+
+ SetShieldWallCPointOrigin( shieldWallFX, < BURN_CARD_WEAPON_HUD_COLOR[0], BURN_CARD_WEAPON_HUD_COLOR[1], BURN_CARD_WEAPON_HUD_COLOR[2] > )
+
+ float tickRate = 0.1
+ float dps = vortexSphere.GetMaxHealth() / duration
+ float dmgAmount = dps * tickRate
+
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+
+ float endSoundTime = endTime - 3.0 // so magic
+ thread PlayDelayedVortexEndSound( endSoundTime, vortexSphere )
+ bool playedEndSound = false
+ vector vortexOrigin = vortexSphere.GetOrigin()
+ entity mover = CreateScriptMover()
+
+ int weaponOwnerTeam = weaponOwner.GetTeam();
+
+ OnThreadEnd(
+ function() : ( vortexSphere, vortexOrigin, endTime, mover, weaponOwnerTeam )
+ {
+ if ( IsValid( vortexSphere ) )
+ {
+ StopSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+ StopSoundOnEntity( vortexSphere, "ShieldWall_End" )
+ }
+
+ if ( IsValid( mover ) )
+ mover.Destroy()
+
+ if ( endTime - Time() >= 1.0 )
+ EmitSoundAtPosition( weaponOwnerTeam, vortexOrigin, "ShieldWall_Destroyed" )
+ }
+ )
+
+ int rampOuts = 3
+ float rampOutTime = 0.75
+ float rampOutFinalFade = 1.0
+ float finalFadeExtraBuffer = 0.45
+
+ wait duration - ( rampOutTime * rampOuts + rampOutFinalFade + finalFadeExtraBuffer )
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_End" )
+
+ entity cpoint = GetShieldWallFXCPoint( shieldWallFX )
+
+ vector cpointOrigin = cpoint.GetOrigin()
+ mover.SetOrigin( cpointOrigin )
+ cpoint.SetParent( mover )
+ float rampTime1 = rampOutTime * 0.75
+ float rampTime2 = rampOutTime - rampTime1
+ for ( int i = 0; i < rampOuts; i++ )
+ {
+ mover.NonPhysicsMoveTo( <100,0,0>, rampTime1, rampTime1, 0.0 )
+ wait rampTime1
+ mover.NonPhysicsMoveTo( cpointOrigin, rampTime2, 0.0, rampTime2 )
+ wait rampTime2
+ }
+
+ mover.NonPhysicsMoveTo( <0,0,0>, rampOutFinalFade, 0.0, 0.0 )
+ wait rampOutFinalFade + finalFadeExtraBuffer
+}
+
+void function PlayDelayedVortexEndSound( float delay, entity vortexSphere )
+{
+ vortexSphere.EndSignal( "OnDestroy" )
+ wait delay
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_End" )
+}
+
+
+function DrainHealthOverTime( entity vortexSphere, entity shieldWallFX, float duration )
+{
+ vortexSphere.EndSignal( "OnDestroy" )
+ shieldWallFX.EndSignal( "OnDestroy" )
+
+ float startTime = Time()
+ float endTime = startTime + duration
+
+ float tickRate = 0.1
+ float dps = vortexSphere.GetMaxHealth() / duration
+ float dmgAmount = dps * tickRate
+
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+
+ float endSoundTime = endTime - 3.0
+ bool playedEndSound = false
+ vector vortexOrigin = vortexSphere.GetOrigin()
+
+ OnThreadEnd(
+ function() : ( vortexSphere, vortexOrigin, endTime )
+ {
+ if ( endTime - Time() < 1.0 )
+ return
+
+ int teamNum = TEAM_UNASSIGNED
+
+ if ( IsValid( vortexSphere ) )
+ {
+ StopSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+ StopSoundOnEntity( vortexSphere, "ShieldWall_End" )
+
+ teamNum = vortexSphere.GetTeam()
+ }
+
+ EmitSoundAtPosition( teamNum, vortexOrigin, "ShieldWall_Destroyed" )
+ }
+ )
+
+ while ( Time() < endTime )
+ {
+ if ( Time() > endSoundTime && !playedEndSound )
+ {
+ EmitSoundOnEntity( vortexSphere, "ShieldWall_End" )
+ playedEndSound = true
+ }
+
+ //vortexSphere.SetHealth( vortexSphere.GetHealth() - dmgAmount )
+ UpdateShieldWallColorForFrac( shieldWallFX, GetHealthFrac( vortexSphere ) )
+ wait tickRate
+ }
+
+ StopSoundOnEntity( vortexSphere, "ShieldWall_Loop" )
+}
+
+function UpdateShieldWallColorForFrac( entity shieldWallFX, float colorFrac )
+{
+ vector color = GetShieldTriLerpColor( 1 - colorFrac )
+
+ if ( IsValid( shieldWallFX ) )
+ SetShieldWallCPointOrigin( shieldWallFX, color )
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+//
+// All functions that care about to-be-deprecated cpoint are below here:
+//
+////////////////////////////////////////////////////////////////////////////////////////////////
+
+void function PlayEffectOnVortexSphere( int fx, vector origin, vector angles, entity vortexSphere )
+{
+ if ( !IsValid( vortexSphere ) )
+ return
+ if ( !IsValid( vortexSphere.e.shieldWallFX ) )
+ return
+ entity cpoint = vortexSphere.e.shieldWallFX.e.cpoint
+ if ( !IsValid( cpoint ) )
+ return
+ StartParticleEffectInWorldWithControlPoint( fx, origin, angles, cpoint.GetOrigin() )
+}
+
+void function SetVortexSphereShieldWallCPoint( entity vortexSphere, entity cpoint )
+{
+ Assert( IsValid( vortexSphere ) )
+ Assert( IsValid( vortexSphere.e.shieldWallFX ) )
+ SetShieldWallCPoint( vortexSphere.e.shieldWallFX, cpoint )
+}
+
+void function SetShieldWallCPoint( entity shieldWallFX, entity cpoint )
+{
+ Assert( IsValid( shieldWallFX ) )
+ Assert( IsValid( cpoint ) )
+ shieldWallFX.e.cpoint = cpoint
+}
+
+void function StopShieldWallFX( entity vortexSphere )
+{
+ entity shieldWallFX = vortexSphere.e.shieldWallFX
+ vortexSphere.e.shieldWallFX = null
+
+ if ( !IsValid( shieldWallFX ) )
+ return
+
+ shieldWallFX.Fire( "StopPlayEndCap" )
+ shieldWallFX.Fire( "Kill", "", 1.0 )
+
+ if ( IsValid( shieldWallFX.e.cpoint ) )
+ shieldWallFX.e.cpoint.Fire( "Kill", "", 1.0 )
+ EffectStop( shieldWallFX )
+}
+
+void function StopShieldWallFXOverTime( entity vortexSphere, float duration )
+{
+ entity shieldWallFX = vortexSphere.e.shieldWallFX
+ shieldWallFX.Fire( "StopPlayEndCap", "", duration )
+ shieldWallFX.Fire( "Kill", "", duration )
+ shieldWallFX.e.cpoint.Fire( "Kill", "", duration )
+}
+
+void function SetShieldWallCPointOrigin( entity shieldWallFX, vector AT_TURRET_SHIELD_COLOR )
+{
+ Assert( IsValid( shieldWallFX ) )
+ if ( !IsValid( shieldWallFX.e.cpoint ) )
+ return
+ shieldWallFX.e.cpoint.SetOrigin( AT_TURRET_SHIELD_COLOR )
+}
+
+entity function GetShieldWallFXCPoint( entity shieldWallFX )
+{
+ Assert( IsValid( shieldWallFX.e.cpoint ) )
+ return shieldWallFX.e.cpoint
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_team_emp.gnut b/Northstar.CustomServers/scripts/vscripts/weapons/_team_emp.gnut
new file mode 100644
index 00000000..41d42848
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_team_emp.gnut
@@ -0,0 +1,38 @@
+global function TeamEMP_Init
+global function EMPEffects
+
+void function TeamEMP_Init()
+{
+ RegisterSignal( "PlayerEMPed" )
+}
+
+void function EMPEffects( entity player, float time )
+{
+ player.nv.empEndTime = Time() + time
+
+ player.Signal( "PlayerEMPed" )
+
+ // remember this is a stack so you need to enable as many times as you disable
+ DisableOffhandWeapons( player )
+
+ thread RecoverFromEMP( player, time )
+}
+
+void function RecoverFromEMP( entity player, float time )
+{
+ svGlobal.levelEnt.EndSignal( "BurnMeter_PreMatchEnter" )
+ player.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( IsValid( player ) )
+ {
+ // remember this is a stack so you need to enable as many times as you disable
+ EnableOffhandWeapons( player )
+ }
+ }
+ )
+
+ wait time + 0.1
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_vortex.nut b/Northstar.CustomServers/scripts/vscripts/weapons/_vortex.nut
new file mode 100644
index 00000000..f1e46a53
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_vortex.nut
@@ -0,0 +1,1983 @@
+untyped
+
+global function Vortex_Init
+
+global function CreateVortexSphere
+global function DestroyVortexSphereFromVortexWeapon
+global function EnableVortexSphere
+#if SERVER
+global function ValidateVortexImpact
+global function TryVortexAbsorb
+global function SetVortexSphereBulletHitRules
+global function SetVortexSphereProjectileHitRules
+#endif
+global function VortexDrainedByImpact
+global function VortexPrimaryAttack
+global function GetVortexSphereCurrentColor
+global function GetShieldTriLerpColor
+global function IsVortexing
+#if SERVER
+global function Vortex_HandleElectricDamage
+global function VortexSphereDrainHealthForDamage
+global function Vortex_CreateImpactEventData
+global function Vortex_SpawnHeatShieldPingFX
+#endif
+
+global function Vortex_SetTagName
+global function Vortex_SetBulletCollectionOffset
+
+global function CodeCallback_OnVortexHitBullet
+global function CodeCallback_OnVortexHitProjectile
+
+const AMPED_WALL_IMPACT_FX = $"P_impact_xo_shield_cp"
+
+global const PROTO_AMPED_WALL = "proto_amped_wall"
+global const GUN_SHIELD_WALL = "gun_shield_wall"
+const PROX_MINE_MODEL = $"models/weapons/caber_shot/caber_shot_thrown.mdl"
+
+const VORTEX_SPHERE_COLOR_CHARGE_FULL = <115, 247, 255> // blue
+const VORTEX_SPHERE_COLOR_CHARGE_MED = <200, 128, 80> // orange
+const VORTEX_SPHERE_COLOR_CHARGE_EMPTY = <200, 80, 80> // red
+const VORTEX_SPHERE_COLOR_PAS_ION_VORTEX = <115, 174, 255> // blue
+const AMPED_DAMAGE_SCALAR = 1.5
+
+const VORTEX_SPHERE_COLOR_CROSSOVERFRAC_FULL2MED = 0.75 // from zero to this fraction, fade between full and medium charge colors
+const VORTEX_SPHERE_COLOR_CROSSOVERFRAC_MED2EMPTY = 0.95 // from "full2med" to this fraction, fade between medium and empty charge colors
+
+const VORTEX_BULLET_ABSORB_COUNT_MAX = 32
+const VORTEX_PROJECTILE_ABSORB_COUNT_MAX = 32
+
+const VORTEX_TIMED_EXPLOSIVE_FUSETIME = 2.75 // fuse time for absorbed projectiles
+const VORTEX_TIMED_EXPLOSIVE_FUSETIME_WARNINGFRAC = 0.75 // wait this fraction of the fuse time before warning the player it's about to explode
+
+const VORTEX_EXP_ROUNDS_RETURN_SPREAD_XY = 0.15
+const VORTEX_EXP_ROUNDS_RETURN_SPREAD_Z = 0.075
+
+const VORTEX_ELECTRIC_DAMAGE_CHARGE_DRAIN_MIN = 0.1 // fraction of charge time
+const VORTEX_ELECTRIC_DAMAGE_CHARGE_DRAIN_MAX = 0.3
+
+//The shotgun spams a lot of pellets that deal too much damage if they return full damage.
+const VORTEX_SHOTGUN_DAMAGE_RATIO = 0.25
+
+
+const SHIELD_WALL_BULLET_FX = $"P_impact_xo_shield_cp"
+const SHIELD_WALL_EXPMED_FX = $"P_impact_exp_med_xo_shield_CP"
+
+const SIGNAL_ID_BULLET_HIT_THINK = "signal_id_bullet_hit_think"
+
+const VORTEX_EXPLOSIVE_WARNING_SFX_LOOP = "Weapon_Vortex_Gun.ExplosiveWarningBeep"
+
+const VORTEX_PILOT_WEAPON_WEAKNESS_DAMAGESCALE = 6.0
+
+// These match the strings in the WeaponEd dropdown box for vortex_refire_behavior
+global const VORTEX_REFIRE_NONE = ""
+global const VORTEX_REFIRE_ABSORB = "absorb"
+global const VORTEX_REFIRE_BULLET = "bullet"
+global const VORTEX_REFIRE_EXPLOSIVE_ROUND = "explosive_round"
+global const VORTEX_REFIRE_ROCKET = "rocket"
+global const VORTEX_REFIRE_GRENADE = "grenade"
+global const VORTEX_REFIRE_GRENADE_LONG_FUSE = "grenade_long_fuse"
+
+const VortexIgnoreClassnames = {
+ ["mp_titancore_flame_wave"] = true,
+ ["mp_ability_grapple"] = true,
+ ["mp_ability_shifter"] = true,
+}
+
+table vortexImpactWeaponInfo
+
+const DEG_COS_60 = cos( 60 * DEG_TO_RAD )
+
+function Vortex_Init()
+{
+ PrecacheParticleSystem( SHIELD_WALL_BULLET_FX )
+ GetParticleSystemIndex( SHIELD_WALL_BULLET_FX )
+ PrecacheParticleSystem( SHIELD_WALL_EXPMED_FX )
+ GetParticleSystemIndex( SHIELD_WALL_EXPMED_FX )
+ PrecacheParticleSystem( AMPED_WALL_IMPACT_FX )
+ GetParticleSystemIndex( AMPED_WALL_IMPACT_FX )
+
+ RegisterSignal( SIGNAL_ID_BULLET_HIT_THINK )
+ RegisterSignal( "VortexStopping" )
+
+ RegisterSignal( "VortexAbsorbed" )
+ RegisterSignal( "VortexFired" )
+ RegisterSignal( "Script_OnDamaged" )
+}
+
+#if SERVER
+var function VortexBulletHitRules_Default( entity vortexSphere, var damageInfo )
+{
+ return damageInfo
+}
+
+bool function VortexProjectileHitRules_Default( entity vortexSphere, entity attacker, bool takesDamageByDefault )
+{
+ return takesDamageByDefault
+}
+
+void function SetVortexSphereBulletHitRules( entity vortexSphere, var functionref( entity, var ) customRules )
+{
+ vortexSphere.e.BulletHitRules = customRules
+}
+
+void function SetVortexSphereProjectileHitRules( entity vortexSphere, bool functionref( entity, entity, bool ) customRules )
+{
+ vortexSphere.e.ProjectileHitRules = customRules
+}
+#endif
+function CreateVortexSphere( entity vortexWeapon, bool useCylinderCheck, bool blockOwnerWeapon, int sphereRadius = 40, int bulletFOV = 180 )
+{
+ entity owner = vortexWeapon.GetWeaponOwner()
+ Assert( owner )
+
+ #if SERVER
+ //printt( "util ent:", vortexWeapon.GetWeaponUtilityEntity() )
+ Assert ( !vortexWeapon.GetWeaponUtilityEntity(), "Tried to create more than one vortex sphere on a vortex weapon!" )
+
+ entity vortexSphere = CreateEntity( "vortex_sphere" )
+ Assert( vortexSphere )
+
+ int spawnFlags = SF_ABSORB_BULLETS | SF_BLOCK_NPC_WEAPON_LOF
+
+ if ( useCylinderCheck )
+ {
+ spawnFlags = spawnFlags | SF_ABSORB_CYLINDER
+ vortexSphere.kv.height = sphereRadius * 2
+ }
+
+ if ( blockOwnerWeapon )
+ spawnFlags = spawnFlags | SF_BLOCK_OWNER_WEAPON
+
+ vortexSphere.kv.spawnflags = spawnFlags
+
+ vortexSphere.kv.enabled = 0
+ vortexSphere.kv.radius = sphereRadius
+ vortexSphere.kv.bullet_fov = bulletFOV
+ vortexSphere.kv.physics_pull_strength = 25
+ vortexSphere.kv.physics_side_dampening = 6
+ vortexSphere.kv.physics_fov = 360
+ vortexSphere.kv.physics_max_mass = 2
+ vortexSphere.kv.physics_max_size = 6
+ Assert( owner.IsNPC() || owner.IsPlayer(), "Vortex script expects the weapon owner to be a player or NPC." )
+
+ SetVortexSphereBulletHitRules( vortexSphere, VortexBulletHitRules_Default )
+ SetVortexSphereProjectileHitRules( vortexSphere, VortexProjectileHitRules_Default )
+
+ DispatchSpawn( vortexSphere )
+
+ vortexSphere.SetOwner( owner )
+
+ if ( owner.IsNPC() )
+ {
+ vortexSphere.SetParent( owner, "PROPGUN" )
+ vortexSphere.SetLocalOrigin( Vector( 0, 35, 0 ) )
+ }
+ else
+ {
+ vortexSphere.SetParent( owner )
+ vortexSphere.SetLocalOrigin( Vector( 0, 10, -30 ) )
+ }
+ vortexSphere.SetAbsAngles( Vector( 0, 0, 0 ) ) //Setting local angles on a parented object is not supported
+
+ vortexSphere.SetOwnerWeapon( vortexWeapon )
+ vortexWeapon.SetWeaponUtilityEntity( vortexSphere )
+ #endif
+
+ SetVortexAmmo( vortexWeapon, 0 )
+}
+
+
+function EnableVortexSphere( entity vortexWeapon )
+{
+ string tagname = GetVortexTagName( vortexWeapon )
+ entity weaponOwner = vortexWeapon.GetWeaponOwner()
+ local hasBurnMod = vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod )
+
+ #if SERVER
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ Assert( vortexSphere )
+ vortexSphere.FireNow( "Enable" )
+
+ thread SetPlayerUsingVortex( weaponOwner, vortexWeapon )
+
+ Vortex_CreateAbsorbFX_ControlPoints( vortexWeapon )
+
+ // world (3P) version of the vortex sphere FX
+ vortexSphere.s.worldFX <- CreateEntity( "info_particle_system" )
+
+ if ( hasBurnMod )
+ {
+ if ( "fxChargingControlPointBurn" in vortexWeapon.s )
+ vortexSphere.s.worldFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxChargingControlPointBurn ) )
+ }
+ else
+ {
+ if ( "fxChargingControlPoint" in vortexWeapon.s )
+ vortexSphere.s.worldFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxChargingControlPoint ) )
+ }
+
+ vortexSphere.s.worldFX.kv.start_active = 1
+ vortexSphere.s.worldFX.SetOwner( weaponOwner )
+ vortexSphere.s.worldFX.SetParent( vortexWeapon, tagname )
+ vortexSphere.s.worldFX.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // not owner only
+ vortexSphere.s.worldFX.kv.cpoint1 = vortexWeapon.s.vortexSphereColorCP.GetTargetName()
+ vortexSphere.s.worldFX.SetStopType( "destroyImmediately" )
+
+ DispatchSpawn( vortexSphere.s.worldFX )
+ #endif
+
+ SetVortexAmmo( vortexWeapon, 0 )
+
+ #if CLIENT
+ if ( IsLocalViewPlayer( weaponOwner ) )
+ {
+ local fxAlias = null
+
+ if ( hasBurnMod )
+ {
+ if ( "fxChargingFPControlPointBurn" in vortexWeapon.s )
+ fxAlias = vortexWeapon.s.fxChargingFPControlPointBurn
+ }
+ else
+ {
+ if ( "fxChargingFPControlPoint" in vortexWeapon.s )
+ fxAlias = vortexWeapon.s.fxChargingFPControlPoint
+ }
+
+ if ( fxAlias )
+ {
+ int sphereClientFXHandle = vortexWeapon.PlayWeaponEffectReturnViewEffectHandle( fxAlias, $"", tagname )
+ thread VortexSphereColorUpdate( vortexWeapon, sphereClientFXHandle )
+ }
+ }
+ #elseif SERVER
+ asset fxAlias = $""
+
+ if ( hasBurnMod )
+ {
+ if ( "fxChargingFPControlPointReplayBurn" in vortexWeapon.s )
+ fxAlias = expect asset( vortexWeapon.s.fxChargingFPControlPointReplayBurn )
+ }
+ else
+ {
+ if ( "fxChargingFPControlPointReplay" in vortexWeapon.s )
+ fxAlias = expect asset( vortexWeapon.s.fxChargingFPControlPointReplay )
+ }
+
+ if ( fxAlias != $"" )
+ vortexWeapon.PlayWeaponEffect( fxAlias, $"", tagname )
+
+ thread VortexSphereColorUpdate( vortexWeapon )
+ #endif
+}
+
+
+function DestroyVortexSphereFromVortexWeapon( entity vortexWeapon )
+{
+ DisableVortexSphereFromVortexWeapon( vortexWeapon )
+
+ #if SERVER
+ DestroyVortexSphere( vortexWeapon.GetWeaponUtilityEntity() )
+ vortexWeapon.SetWeaponUtilityEntity( null )
+ #endif
+}
+
+void function DestroyVortexSphere( entity vortexSphere )
+{
+ if ( IsValid( vortexSphere ) )
+ {
+ vortexSphere.s.worldFX.Destroy()
+ vortexSphere.Destroy()
+ }
+}
+
+
+function DisableVortexSphereFromVortexWeapon( entity vortexWeapon )
+{
+ vortexWeapon.Signal( "VortexStopping" )
+
+ // server cleanup
+ #if SERVER
+ DisableVortexSphere( vortexWeapon.GetWeaponUtilityEntity() )
+ Vortex_CleanupAllEffects( vortexWeapon )
+ Vortex_ClearImpactEventData( vortexWeapon )
+ #endif
+
+ // client & server cleanup
+
+ if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ {
+ if ( "fxChargingFPControlPointBurn" in vortexWeapon.s )
+ vortexWeapon.StopWeaponEffect( expect asset( vortexWeapon.s.fxChargingFPControlPointBurn ), $"" )
+ if ( "fxChargingFPControlPointReplayBurn" in vortexWeapon.s )
+ vortexWeapon.StopWeaponEffect( expect asset( vortexWeapon.s.fxChargingFPControlPointReplayBurn ), $"" )
+ }
+ else
+ {
+ if ( "fxChargingFPControlPoint" in vortexWeapon.s )
+ vortexWeapon.StopWeaponEffect( expect asset( vortexWeapon.s.fxChargingFPControlPoint ), $"" )
+ if ( "fxChargingFPControlPointReplay" in vortexWeapon.s )
+ vortexWeapon.StopWeaponEffect( expect asset( vortexWeapon.s.fxChargingFPControlPointReplay ), $"" )
+ }
+}
+
+void function DisableVortexSphere( entity vortexSphere )
+{
+ if ( IsValid( vortexSphere ) )
+ {
+ vortexSphere.FireNow( "Disable" )
+ vortexSphere.Signal( SIGNAL_ID_BULLET_HIT_THINK )
+ }
+
+}
+
+
+#if SERVER
+function Vortex_CreateAbsorbFX_ControlPoints( entity vortexWeapon )
+{
+ entity player = vortexWeapon.GetWeaponOwner()
+ Assert( player )
+
+ // vortex swirling incoming rounds FX location control point
+ if ( !( "vortexBulletEffectCP" in vortexWeapon.s ) )
+ vortexWeapon.s.vortexBulletEffectCP <- null
+ vortexWeapon.s.vortexBulletEffectCP = CreateEntity( "info_placement_helper" )
+ SetTargetName( expect entity( vortexWeapon.s.vortexBulletEffectCP ), UniqueString( "vortexBulletEffectCP" ) )
+ vortexWeapon.s.vortexBulletEffectCP.kv.start_active = 1
+
+ DispatchSpawn( vortexWeapon.s.vortexBulletEffectCP )
+
+ vector offset = GetBulletCollectionOffset( vortexWeapon )
+ vector origin = player.OffsetPositionFromView( player.EyePosition(), offset )
+
+ vortexWeapon.s.vortexBulletEffectCP.SetOrigin( origin )
+ vortexWeapon.s.vortexBulletEffectCP.SetParent( player )
+
+ // vortex sphere color control point
+ if ( !( "vortexSphereColorCP" in vortexWeapon.s ) )
+ vortexWeapon.s.vortexSphereColorCP <- null
+ vortexWeapon.s.vortexSphereColorCP = CreateEntity( "info_placement_helper" )
+ SetTargetName( expect entity( vortexWeapon.s.vortexSphereColorCP ), UniqueString( "vortexSphereColorCP" ) )
+ vortexWeapon.s.vortexSphereColorCP.kv.start_active = 1
+
+ DispatchSpawn( vortexWeapon.s.vortexSphereColorCP )
+}
+
+
+function Vortex_CleanupAllEffects( entity vortexWeapon )
+{
+ Assert( IsServer() )
+
+ Vortex_CleanupImpactAbsorbFX( vortexWeapon )
+
+ if ( ( "vortexBulletEffectCP" in vortexWeapon.s ) && IsValid_ThisFrame( expect entity( vortexWeapon.s.vortexBulletEffectCP ) ) )
+ vortexWeapon.s.vortexBulletEffectCP.Destroy()
+
+ if ( ( "vortexSphereColorCP" in vortexWeapon.s ) && IsValid_ThisFrame( expect entity( vortexWeapon.s.vortexSphereColorCP ) ) )
+ vortexWeapon.s.vortexSphereColorCP.Destroy()
+}
+#endif // SERVER
+
+
+function SetPlayerUsingVortex( entity weaponOwner, entity vortexWeapon )
+{
+ weaponOwner.EndSignal( "OnDeath" )
+
+ weaponOwner.s.isVortexing <- true
+
+ vortexWeapon.WaitSignal( "VortexStopping" )
+
+ OnThreadEnd
+ (
+ function() : ( weaponOwner )
+ {
+ if ( IsValid_ThisFrame( weaponOwner ) && "isVortexing" in weaponOwner.s )
+ {
+ delete weaponOwner.s.isVortexing
+ }
+ }
+ )
+}
+
+
+function IsVortexing( entity ent )
+{
+ Assert( IsServer() )
+
+ if ( "isVortexing" in ent.s )
+ return true
+}
+
+
+#if SERVER
+function Vortex_HandleElectricDamage( entity ent, entity attacker, damage, entity weapon )
+{
+ if ( !IsValid( ent ) )
+ return damage
+
+ if ( !ent.IsTitan() )
+ return damage
+
+ if ( !ent.IsPlayer() && !ent.IsNPC() )
+ return damage
+
+ if ( !IsVortexing( ent ) )
+ return damage
+
+ entity vortexWeapon = ent.GetActiveWeapon()
+ if ( !IsValid( vortexWeapon ) )
+ return damage
+
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !IsValid( vortexSphere ) )
+ return damage
+
+ if ( !IsValid( vortexWeapon ) || !IsValid( vortexSphere ) )
+ return damage
+
+ // vortex FOV check
+ //printt( "sphere FOV:", vortexSphere.kv.bullet_fov )
+ local sphereFOV = vortexSphere.kv.bullet_fov.tointeger()
+ entity attackerWeapon = attacker.GetActiveWeapon()
+ int attachIdx = attackerWeapon.LookupAttachment( "muzzle_flash" )
+ vector beamOrg = attackerWeapon.GetAttachmentOrigin( attachIdx )
+ vector firingDir = beamOrg - vortexSphere.GetOrigin()
+ firingDir = Normalize( firingDir )
+ vector vortexDir = AnglesToForward( vortexSphere.GetAngles() )
+
+ float dot = DotProduct( vortexDir, firingDir )
+
+ float degCos = DEG_COS_60
+ if ( sphereFOV != 120 )
+ deg_cos( sphereFOV * 0.5 )
+
+ // not in the vortex cone
+ if ( dot < degCos )
+ return damage
+
+ if ( "fxElectricalExplosion" in vortexWeapon.s )
+ {
+ entity fxRef = CreateEntity( "info_particle_system" )
+ fxRef.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxElectricalExplosion ) )
+ fxRef.kv.start_active = 1
+ fxRef.SetStopType( "destroyImmediately" )
+ //fxRef.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER // HACK this turns on owner only visibility. Uncomment when we hook up dedicated 3P effects
+ fxRef.SetOwner( ent )
+ fxRef.SetOrigin( vortexSphere.GetOrigin() )
+ fxRef.SetParent( ent )
+
+ DispatchSpawn( fxRef )
+ fxRef.Kill_Deprecated_UseDestroyInstead( 1 )
+ }
+
+ return 0
+}
+
+// this function handles all incoming vortex impact events
+bool function TryVortexAbsorb( entity vortexSphere, entity attacker, vector origin, int damageSourceID, entity weapon, string weaponName, string impactType, entity projectile = null, damageType = null, reflect = false )
+{
+ if ( weaponName in VortexIgnoreClassnames )
+ return false
+
+ entity vortexWeapon = vortexSphere.GetOwnerWeapon()
+ entity owner = vortexWeapon.GetWeaponOwner()
+
+ // keep cycling the oldest hitscan bullets out
+ if( !reflect )
+ {
+ if ( impactType == "hitscan" )
+ Vortex_ClampAbsorbedBulletCount( vortexWeapon )
+ else
+ Vortex_ClampAbsorbedProjectileCount( vortexWeapon )
+ }
+
+ // vortex spheres tag refired projectiles with info about the original projectile for accurate duplication when re-absorbed
+ if ( projectile )
+ {
+
+ // specifically for tether, since it gets moved to the vortex area and can get absorbed in the process, then destroyed
+ if ( !IsValid( projectile ) )
+ return false
+
+ entity projOwner = projectile.GetOwner()
+ if ( IsValid( projOwner ) && projOwner.GetTeam() == owner.GetTeam() )
+ return false
+
+ if ( projectile.proj.hasBouncedOffVortex )
+ return false
+
+ if ( projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_ignores_vortex" ) == "fall_vortex" )
+ {
+ vector velocity = projectile.GetVelocity()
+ vector multiplier = < -0.25, -0.25, -0.25 >
+ velocity = < velocity.x * multiplier.x, velocity.y * multiplier.y, velocity.z * multiplier.z >
+ projectile.SetVelocity( velocity )
+ projectile.proj.hasBouncedOffVortex = true
+ return false
+ }
+
+ // if ( projectile.GetParent() == owner )
+ // return false
+
+ if ( "originalDamageSource" in projectile.s )
+ {
+ damageSourceID = expect int( projectile.s.originalDamageSource )
+
+ // Vortex Volley Achievement
+ if ( IsValid( owner ) && owner.IsPlayer() )
+ {
+ //if ( PlayerProgressionAllowed( owner ) )
+ // SetAchievement( owner, "ach_vortexVolley", true )
+ }
+ }
+
+ // Max projectile stat tracking
+ int projectilesInVortex = 1
+ projectilesInVortex += vortexWeapon.w.vortexImpactData.len()
+
+ if ( IsValid( owner ) && owner.IsPlayer() )
+ {
+ if ( PlayerProgressionAllowed( owner ) )
+ {
+ int record = owner.GetPersistentVarAsInt( "mostProjectilesCollectedInVortex" )
+ if ( projectilesInVortex > record )
+ owner.SetPersistentVar( "mostProjectilesCollectedInVortex", projectilesInVortex )
+ }
+
+ var impact_sound_1p = projectile.ProjectileGetWeaponInfoFileKeyField( "vortex_impact_sound_1p" )
+ if ( impact_sound_1p != null )
+ EmitSoundOnEntityOnlyToPlayer( vortexSphere, owner, impact_sound_1p )
+ }
+
+ var impact_sound_3p = projectile.ProjectileGetWeaponInfoFileKeyField( "vortex_impact_sound_3p" )
+ if ( impact_sound_3p != null )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, impact_sound_3p )
+ }
+ else
+ {
+ if ( IsValid( owner ) && owner.IsPlayer() )
+ {
+ var impact_sound_1p = GetWeaponInfoFileKeyField_Global( weaponName, "vortex_impact_sound_1p" )
+ if ( impact_sound_1p != null )
+ EmitSoundOnEntityOnlyToPlayer( vortexSphere, owner, impact_sound_1p )
+ }
+
+ var impact_sound_3p = GetWeaponInfoFileKeyField_Global( weaponName, "vortex_impact_sound_3p" )
+ if ( impact_sound_3p != null )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, origin, impact_sound_3p )
+ }
+
+ local impactData = Vortex_CreateImpactEventData( vortexWeapon, attacker, origin, damageSourceID, weaponName, impactType )
+
+ VortexDrainedByImpact( vortexWeapon, weapon, projectile, damageType )
+ Vortex_NotifyAttackerDidDamage( expect entity( impactData.attacker ), owner, impactData.origin )
+
+ if ( impactData.refireBehavior == VORTEX_REFIRE_ABSORB )
+ return true
+
+ if ( vortexWeapon.GetWeaponClassName() == "mp_titanweapon_heat_shield" )
+ return true
+
+ if ( !Vortex_ScriptCanHandleImpactEvent( impactData ) )
+ return false
+
+ Vortex_StoreImpactEvent( vortexWeapon, impactData )
+
+ VortexImpact_PlayAbsorbedFX( vortexWeapon, impactData )
+
+ if ( impactType == "hitscan" )
+ vortexSphere.AddBulletToSphere();
+ else
+ vortexSphere.AddProjectileToSphere();
+
+ local maxShotgunPelletsToIgnore = VORTEX_BULLET_ABSORB_COUNT_MAX * ( 1 - VORTEX_SHOTGUN_DAMAGE_RATIO )
+ if ( IsPilotShotgunWeapon( weaponName ) && ( vortexWeapon.s.shotgunPelletsToIgnore + 1 ) < maxShotgunPelletsToIgnore )
+ vortexWeapon.s.shotgunPelletsToIgnore += ( 1 - VORTEX_SHOTGUN_DAMAGE_RATIO )
+
+ if ( reflect )
+ {
+ local attackParams = {}
+ attackParams.pos <- owner.EyePosition()
+ attackParams.dir <- owner.GetPlayerOrNPCViewVector()
+
+ int bulletsFired = VortexReflectAttack( vortexWeapon, attackParams, expect vector( impactData.origin ) )
+
+ Vortex_CleanupImpactAbsorbFX( vortexWeapon )
+ Vortex_ClearImpactEventData( vortexWeapon )
+
+ while ( vortexSphere.GetBulletAbsorbedCount() > 0 )
+ vortexSphere.RemoveBulletFromSphere();
+
+ while ( vortexSphere.GetProjectileAbsorbedCount() > 0 )
+ vortexSphere.RemoveProjectileFromSphere();
+ }
+
+ return true
+}
+#endif // SERVER
+
+function VortexDrainedByImpact( entity vortexWeapon, entity weapon, entity projectile, damageType )
+{
+ if ( vortexWeapon.HasMod( "unlimited_charge_time" ) )
+ return
+ if ( vortexWeapon.HasMod( "vortex_extended_effect_and_no_use_penalty" ) )
+ return
+
+ float amount
+ if ( projectile )
+ amount = projectile.GetProjectileWeaponSettingFloat( eWeaponVar.vortex_drain )
+ else
+ amount = weapon.GetWeaponSettingFloat( eWeaponVar.vortex_drain )
+
+ if ( amount <= 0.0 )
+ return
+
+ if ( vortexWeapon.GetWeaponClassName() == "mp_titanweapon_vortex_shield_ion" )
+ {
+ entity owner = vortexWeapon.GetWeaponOwner()
+ int totalEnergy = owner.GetSharedEnergyTotal()
+ owner.TakeSharedEnergy( int( float( totalEnergy ) * amount ) )
+ }
+ else
+ {
+ float frac = min ( vortexWeapon.GetWeaponChargeFraction() + amount, 1.0 )
+ vortexWeapon.SetWeaponChargeFraction( frac )
+ }
+}
+
+
+function VortexSlowOwnerFromAttacker( entity player, entity attacker, vector velocity, float multiplier )
+{
+ vector damageForward = player.GetOrigin() - attacker.GetOrigin()
+ damageForward.z = 0
+ damageForward.Norm()
+
+ vector velForward = player.GetVelocity()
+ velForward.z = 0
+ velForward.Norm()
+
+ float dot = DotProduct( velForward, damageForward )
+ if ( dot >= -0.5 )
+ return
+
+ dot += 0.5
+ dot *= -2.0
+
+ vector negateVelocity = velocity * -multiplier
+ negateVelocity *= dot
+
+ velocity += negateVelocity
+ player.SetVelocity( velocity )
+}
+
+
+#if SERVER
+function Vortex_ClampAbsorbedBulletCount( entity vortexWeapon )
+{
+ if ( GetBulletsAbsorbedCount( vortexWeapon ) >= ( VORTEX_BULLET_ABSORB_COUNT_MAX - 1 ) )
+ Vortex_RemoveOldestAbsorbedBullet( vortexWeapon )
+}
+
+function Vortex_ClampAbsorbedProjectileCount( entity vortexWeapon )
+{
+ if ( GetProjectilesAbsorbedCount( vortexWeapon ) >= ( VORTEX_PROJECTILE_ABSORB_COUNT_MAX - 1 ) )
+ Vortex_RemoveOldestAbsorbedProjectile( vortexWeapon )
+}
+
+function Vortex_RemoveOldestAbsorbedBullet( entity vortexWeapon )
+{
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+
+ local bulletImpacts = Vortex_GetHitscanBulletImpacts( vortexWeapon )
+ local impactDataToRemove = bulletImpacts[ 0 ] // since it's an array, the first one will be the oldest
+
+ Vortex_RemoveImpactEvent( vortexWeapon, impactDataToRemove )
+
+ vortexSphere.RemoveBulletFromSphere()
+}
+
+function Vortex_RemoveOldestAbsorbedProjectile( entity vortexWeapon )
+{
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+
+ local projImpacts = Vortex_GetProjectileImpacts( vortexWeapon )
+ local impactDataToRemove = projImpacts[ 0 ] // since it's an array, the first one will be the oldest
+
+ Vortex_RemoveImpactEvent( vortexWeapon, impactDataToRemove )
+
+ vortexSphere.RemoveProjectileFromSphere()
+}
+
+function Vortex_CreateImpactEventData( entity vortexWeapon, entity attacker, vector origin, int damageSourceID, string weaponName, string impactType )
+{
+ entity player = vortexWeapon.GetWeaponOwner()
+ local impactData = {}
+
+ impactData.attacker <- attacker
+ impactData.origin <- origin
+ impactData.damageSourceID <- damageSourceID
+ impactData.weaponName <- weaponName
+ impactData.impactType <- impactType
+
+ impactData.refireBehavior <- VORTEX_REFIRE_NONE
+ impactData.absorbSFX <- "Vortex_Shield_AbsorbBulletSmall"
+ impactData.absorbSFX_1p_vs_3p <- null
+
+ impactData.team <- null
+ // sets a team even if the attacker disconnected
+ if ( IsValid_ThisFrame( attacker ) )
+ {
+ impactData.team = attacker.GetTeam()
+ }
+ else
+ {
+ // default to opposite team
+ if ( player.GetTeam() == TEAM_IMC )
+ impactData.team = TEAM_MILITIA
+ else
+ impactData.team = TEAM_IMC
+ }
+
+ impactData.absorbFX <- null
+ impactData.absorbFX_3p <- null
+ impactData.fxEnt_absorb <- null
+
+ impactData.explosionradius <- null
+ impactData.explosion_damage <- null
+ impactData.impact_effect_table <- -1
+ // -- everything from here down relies on being able to read a megaweapon file
+ if ( !( impactData.weaponName in vortexImpactWeaponInfo ) )
+ {
+ vortexImpactWeaponInfo[ impactData.weaponName ] <- {}
+ vortexImpactWeaponInfo[ impactData.weaponName ].absorbFX <- GetWeaponInfoFileKeyFieldAsset_Global( impactData.weaponName, "vortex_absorb_effect" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].absorbFX_3p <- GetWeaponInfoFileKeyFieldAsset_Global( impactData.weaponName, "vortex_absorb_effect_third_person" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].refireBehavior <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "vortex_refire_behavior" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].absorbSound <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "vortex_absorb_sound" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].absorbSound_1p_vs_3p <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "vortex_absorb_sound_1p_vs_3p" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].explosionradius <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "explosionradius" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].explosion_damage_heavy_armor <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "explosion_damage_heavy_armor" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].explosion_damage <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "explosion_damage" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].impact_effect_table <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "impact_effect_table" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].grenade_ignition_time <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "grenade_ignition_time" )
+ vortexImpactWeaponInfo[ impactData.weaponName ].grenade_fuse_time <- GetWeaponInfoFileKeyField_Global( impactData.weaponName, "grenade_fuse_time" )
+ }
+
+ impactData.absorbFX = vortexImpactWeaponInfo[ impactData.weaponName ].absorbFX
+ impactData.absorbFX_3p = vortexImpactWeaponInfo[ impactData.weaponName ].absorbFX_3p
+ if ( impactData.absorbFX )
+ Assert( impactData.absorbFX_3p, "Missing 3rd person absorb effect for " + impactData.weaponName )
+ impactData.refireBehavior = vortexImpactWeaponInfo[ impactData.weaponName ].refireBehavior
+
+ local absorbSound = vortexImpactWeaponInfo[ impactData.weaponName ].absorbSound
+ if ( absorbSound )
+ impactData.absorbSFX = absorbSound
+
+ local absorbSound_1p_vs_3p = vortexImpactWeaponInfo[ impactData.weaponName ].absorbSound_1p_vs_3p
+ if ( absorbSound_1p_vs_3p )
+ impactData.absorbSFX_1p_vs_3p = absorbSound_1p_vs_3p
+
+ // info we need for refiring (some types of) impacts
+ impactData.explosionradius = vortexImpactWeaponInfo[ impactData.weaponName ].explosionradius
+ impactData.explosion_damage = vortexImpactWeaponInfo[ impactData.weaponName ].explosion_damage_heavy_armor
+ if ( impactData.explosion_damage == null )
+ impactData.explosion_damage = vortexImpactWeaponInfo[ impactData.weaponName ].explosion_damage
+ impactData.impact_effect_table = vortexImpactWeaponInfo[ impactData.weaponName ].impact_effect_table
+
+ return impactData
+}
+
+function Vortex_ScriptCanHandleImpactEvent( impactData )
+{
+ if ( impactData.refireBehavior == VORTEX_REFIRE_NONE )
+ return false
+
+ if ( !impactData.absorbFX )
+ return false
+
+ if ( impactData.impactType == "projectile" && !impactData.impact_effect_table )
+ return false
+
+ return true
+}
+
+function Vortex_StoreImpactEvent( entity vortexWeapon, impactData )
+{
+ vortexWeapon.w.vortexImpactData.append( impactData )
+}
+
+// safely removes data for a single impact event
+function Vortex_RemoveImpactEvent( entity vortexWeapon, impactData )
+{
+ Vortex_ImpactData_KillAbsorbFX( impactData )
+
+ vortexWeapon.w.vortexImpactData.fastremovebyvalue( impactData )
+}
+
+function Vortex_GetAllImpactEvents( entity vortexWeapon )
+{
+ return vortexWeapon.w.vortexImpactData
+}
+
+function Vortex_ClearImpactEventData( entity vortexWeapon )
+{
+ vortexWeapon.w.vortexImpactData = []
+}
+
+function VortexImpact_PlayAbsorbedFX( entity vortexWeapon, impactData )
+{
+ // generic shield ping FX
+ Vortex_SpawnShieldPingFX( vortexWeapon, impactData )
+
+ // specific absorb FX
+ impactData.fxEnt_absorb = Vortex_SpawnImpactAbsorbFX( vortexWeapon, impactData )
+}
+
+// FX played when something first enters the vortex sphere
+function Vortex_SpawnShieldPingFX( entity vortexWeapon, impactData )
+{
+ entity player = vortexWeapon.GetWeaponOwner()
+ Assert( player )
+
+ local absorbSFX = impactData.absorbSFX
+ //printt( "SFX absorb sound:", absorbSFX )
+ if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ EmitSoundOnEntity( vortexWeapon, "Vortex_Shield_Deflect_Amped" )
+ else
+ {
+ EmitSoundOnEntity( vortexWeapon, absorbSFX )
+ if ( impactData.absorbSFX_1p_vs_3p != null )
+ {
+ if ( IsValid( impactData.attacker ) && impactData.attacker.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( vortexWeapon, impactData.attacker, impactData.absorbSFX_1p_vs_3p )
+ }
+ }
+ }
+
+ entity pingFX = CreateEntity( "info_particle_system" )
+
+ if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ {
+ if ( "fxBulletHitBurn" in vortexWeapon.s )
+ pingFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxBulletHitBurn ) )
+ }
+ else
+ {
+ if ( "fxBulletHit" in vortexWeapon.s )
+ pingFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxBulletHit ) )
+ }
+
+ pingFX.kv.start_active = 1
+
+ DispatchSpawn( pingFX )
+
+ pingFX.SetOrigin( impactData.origin )
+ pingFX.SetParent( player )
+ pingFX.Kill_Deprecated_UseDestroyInstead( 0.25 )
+}
+
+function Vortex_SpawnHeatShieldPingFX( entity vortexWeapon, impactData, bool impactTypeIsBullet )
+{
+ entity player = vortexWeapon.GetWeaponOwner()
+ Assert( player )
+
+ if ( impactTypeIsBullet )
+ EmitSoundOnEntity( vortexWeapon, "heat_shield_stop_bullet" )
+ else
+ EmitSoundOnEntity( vortexWeapon, "heat_shield_stop_projectile" )
+
+ entity pingFX = CreateEntity( "info_particle_system" )
+
+ if ( "fxBulletHit" in vortexWeapon.s )
+ pingFX.SetValueForEffectNameKey( expect asset( vortexWeapon.s.fxBulletHit ) )
+
+ pingFX.kv.start_active = 1
+
+ DispatchSpawn( pingFX )
+
+ pingFX.SetOrigin( impactData.origin )
+ pingFX.SetParent( player )
+ pingFX.Kill_Deprecated_UseDestroyInstead( 0.25 )
+}
+
+function Vortex_SpawnImpactAbsorbFX( entity vortexWeapon, impactData )
+{
+ // in case we're in the middle of cleaning the weapon up
+ if ( !IsValid( vortexWeapon.s.vortexBulletEffectCP ) )
+ return
+
+ entity owner = vortexWeapon.GetWeaponOwner()
+ Assert( owner )
+
+ local fxRefs = []
+
+ // owner
+ {
+ entity fxRef = CreateEntity( "info_particle_system" )
+
+ fxRef.SetValueForEffectNameKey( expect asset( impactData.absorbFX ) )
+ fxRef.kv.start_active = 1
+ fxRef.SetStopType( "destroyImmediately" )
+ fxRef.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ fxRef.kv.cpoint1 = vortexWeapon.s.vortexBulletEffectCP.GetTargetName()
+
+ DispatchSpawn( fxRef )
+
+ fxRef.SetOwner( owner )
+ fxRef.SetOrigin( impactData.origin )
+ fxRef.SetParent( owner )
+
+ fxRefs.append( fxRef )
+ }
+
+ // everyone else
+ {
+ entity fxRef = CreateEntity( "info_particle_system" )
+
+ fxRef.SetValueForEffectNameKey( expect asset( impactData.absorbFX_3p ) )
+ fxRef.kv.start_active = 1
+ fxRef.SetStopType( "destroyImmediately" )
+ fxRef.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // other only visibility
+ fxRef.kv.cpoint1 = vortexWeapon.s.vortexBulletEffectCP.GetTargetName()
+
+ DispatchSpawn( fxRef )
+
+ fxRef.SetOwner( owner )
+ fxRef.SetOrigin( impactData.origin )
+ fxRef.SetParent( owner )
+
+ fxRefs.append( fxRef )
+ }
+
+ return fxRefs
+}
+
+function Vortex_CleanupImpactAbsorbFX( entity vortexWeapon )
+{
+ foreach ( impactData in Vortex_GetAllImpactEvents( vortexWeapon ) )
+ {
+ Vortex_ImpactData_KillAbsorbFX( impactData )
+ }
+}
+
+function Vortex_ImpactData_KillAbsorbFX( impactData )
+{
+ foreach ( fxRef in impactData.fxEnt_absorb )
+ {
+ if ( !IsValid( fxRef ) )
+ continue
+
+ fxRef.Fire( "DestroyImmediately" )
+ fxRef.Kill_Deprecated_UseDestroyInstead()
+ }
+}
+
+bool function PlayerDiedOrDisconnected( entity player )
+{
+ if ( !IsValid( player ) )
+ return true
+
+ if ( !IsAlive( player ) )
+ return true
+
+ if ( IsDisconnected( player ) )
+ return true
+
+ return false
+}
+
+#endif // SERVER
+
+int function VortexPrimaryAttack( entity vortexWeapon, WeaponPrimaryAttackParams attackParams )
+{
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !vortexSphere )
+ return 0
+
+ #if SERVER
+ Assert( vortexSphere )
+ #endif
+
+ int totalfired = 0
+ int totalAttempts = 0
+
+ bool forceReleased = false
+ // in this case, it's also considered "force released" if the charge time runs out
+ if ( vortexWeapon.IsForceRelease() || vortexWeapon.GetWeaponChargeFraction() == 1 )
+ forceReleased = true
+
+ // PREDICTED REFIRES
+ // bullet impact events don't individually fire back per event because we aggregate and then shotgun blast them
+ int bulletsFired = Vortex_FireBackBullets( vortexWeapon, attackParams )
+ totalfired += bulletsFired
+
+ // UNPREDICTED REFIRES
+ #if SERVER
+ //printt( "server: force released?", forceReleased )
+
+ local unpredictedRefires = Vortex_GetProjectileImpacts( vortexWeapon )
+
+ // HACK we don't actually want to refire them with a spiral but
+ // this is to temporarily ensure compatibility with the Titan rocket launcher
+ if ( !( "spiralMissileIdx" in vortexWeapon.s ) )
+ vortexWeapon.s.spiralMissileIdx <- null
+ vortexWeapon.s.spiralMissileIdx = 0
+
+ foreach ( impactData in unpredictedRefires )
+ {
+ table fakeAttackParams = {pos = attackParams.pos, dir = attackParams.dir, firstTimePredicted = attackParams.firstTimePredicted, burstIndex = attackParams.burstIndex}
+ bool didFire = DoVortexAttackForImpactData( vortexWeapon, fakeAttackParams, impactData, totalAttempts )
+ if ( didFire )
+ totalfired++
+ totalAttempts++
+ }
+ //printt( "totalfired", totalfired )
+ #else
+ totalfired += GetProjectilesAbsorbedCount( vortexWeapon )
+ #endif
+
+ SetVortexAmmo( vortexWeapon, 0 )
+
+ vortexWeapon.Signal( "VortexFired" )
+
+ if ( forceReleased )
+ DestroyVortexSphereFromVortexWeapon( vortexWeapon )
+ else
+ DisableVortexSphereFromVortexWeapon( vortexWeapon )
+
+ return totalfired
+}
+
+int function Vortex_FireBackBullets( entity vortexWeapon, WeaponPrimaryAttackParams attackParams )
+{
+ int bulletCount = GetBulletsAbsorbedCount( vortexWeapon )
+ //Defensive Check - Couldn't repro error.
+ if ( "shotgunPelletsToIgnore" in vortexWeapon.s )
+ bulletCount = int( ceil( bulletCount - vortexWeapon.s.shotgunPelletsToIgnore ) )
+
+ if ( bulletCount )
+ {
+ bulletCount = minint( bulletCount, MAX_BULLET_PER_SHOT )
+
+ //if ( IsClient() && GetLocalViewPlayer() == vortexWeapon.GetWeaponOwner() )
+ // printt( "vortex firing", bulletCount, "bullets" )
+
+ float radius = LOUD_WEAPON_AI_SOUND_RADIUS_MP;
+ vortexWeapon.EmitWeaponNpcSound( radius, 0.2 )
+ int damageType = damageTypes.shotgun | DF_VORTEX_REFIRE
+ if ( bulletCount == 1 )
+ vortexWeapon.FireWeaponBullet( attackParams.pos, attackParams.dir, bulletCount, damageType )
+ else
+ ShotgunBlast( vortexWeapon, attackParams.pos, attackParams.dir, bulletCount, damageType )
+ }
+
+ return bulletCount
+}
+
+#if SERVER
+bool function Vortex_FireBackExplosiveRound( vortexWeapon, attackParams, impactData, sequenceID )
+{
+ expect entity( vortexWeapon )
+
+ // common projectile data
+ float projSpeed = 8000.0
+ int damageType = damageTypes.explosive | DF_VORTEX_REFIRE
+
+ vortexWeapon.EmitWeaponSound( "Weapon.Explosion_Med" )
+
+ vector attackPos
+ //Requires code feature to properly fire tracers from offset positions.
+ //if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ // attackPos = impactData.origin
+ //else
+ attackPos = Vortex_GenerateRandomRefireOrigin( vortexWeapon )
+
+ vector fireVec = Vortex_GenerateRandomRefireVector( vortexWeapon, VORTEX_EXP_ROUNDS_RETURN_SPREAD_XY, VORTEX_EXP_ROUNDS_RETURN_SPREAD_Z )
+
+ // fire off the bolt
+ entity bolt = vortexWeapon.FireWeaponBolt( attackPos, fireVec, projSpeed, damageType, damageType, PROJECTILE_NOT_PREDICTED, sequenceID )
+ if ( bolt )
+ {
+ bolt.kv.gravity = 0.3
+
+ Vortex_ProjectileCommonSetup( bolt, impactData )
+ }
+
+ return true
+}
+
+bool function Vortex_FireBackProjectileBullet( vortexWeapon, attackParams, impactData, sequenceID )
+{
+ expect entity( vortexWeapon )
+
+ // common projectile data
+ float projSpeed = 12000.0
+ int damageType = damageTypes.bullet | DF_VORTEX_REFIRE
+
+ vortexWeapon.EmitWeaponSound( "Weapon.Explosion_Med" )
+
+ vector attackPos
+ //Requires code feature to properly fire tracers from offset positions.
+ //if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ // attackPos = impactData.origin
+ //else
+ attackPos = Vortex_GenerateRandomRefireOrigin( vortexWeapon )
+
+ vector fireVec = Vortex_GenerateRandomRefireVector( vortexWeapon, 0.15, 0.1 )
+ //printt( Time(), fireVec ) // print for bug with random
+
+ // fire off the bolt
+ entity bolt = vortexWeapon.FireWeaponBolt( attackPos, fireVec, projSpeed, damageType, damageType, PROJECTILE_NOT_PREDICTED, sequenceID )
+ if ( bolt )
+ {
+ bolt.kv.gravity = 0.0
+
+ Vortex_ProjectileCommonSetup( bolt, impactData )
+ }
+
+ return true
+}
+
+vector function Vortex_GenerateRandomRefireOrigin( entity vortexWeapon, float distFromCenter = 3.0 )
+{
+ float distFromCenter_neg = distFromCenter * -1
+
+ vector attackPos = expect vector( vortexWeapon.s.vortexBulletEffectCP.GetOrigin() )
+
+ float x = RandomFloatRange( distFromCenter_neg, distFromCenter )
+ float y = RandomFloatRange( distFromCenter_neg, distFromCenter )
+ float z = RandomFloatRange( distFromCenter_neg, distFromCenter )
+
+ attackPos = attackPos + Vector( x, y, z )
+
+ return attackPos
+}
+
+vector function Vortex_GenerateRandomRefireVector( entity vortexWeapon, float vecSpread, float vecSpreadZ )
+{
+ float x = RandomFloatRange( vecSpread * -1, vecSpread )
+ float y = RandomFloatRange( vecSpread * -1, vecSpread )
+ float z = RandomFloatRange( vecSpreadZ * -1, vecSpreadZ )
+
+ vector fireVec = vortexWeapon.GetWeaponOwner().GetPlayerOrNPCViewVector() + Vector( x, y, z )
+ return fireVec
+}
+
+bool function Vortex_FireBackRocket( vortexWeapon, attackParams, impactData, sequenceID )
+{
+ expect entity( vortexWeapon )
+
+ // TODO prediction for clients
+ Assert( IsServer() )
+
+ entity rocket = vortexWeapon.FireWeaponMissile( attackParams.pos, attackParams.dir, 1800.0, damageTypes.largeCaliberExp | DF_VORTEX_REFIRE, damageTypes.largeCaliberExp | DF_VORTEX_REFIRE, false, PROJECTILE_NOT_PREDICTED )
+
+ if ( rocket )
+ {
+ rocket.kv.lifetime = RandomFloatRange( 2.6, 3.5 )
+
+ InitMissileForRandomDriftForVortexLow( rocket, expect vector( attackParams.pos ), expect vector( attackParams.dir ) )
+
+ Vortex_ProjectileCommonSetup( rocket, impactData )
+ }
+
+ return true
+}
+
+bool function Vortex_FireBackGrenade( entity vortexWeapon, attackParams, impactData, int attackSeedCount, float baseFuseTime )
+{
+ float x = RandomFloatRange( -0.2, 0.2 )
+ float y = RandomFloatRange( -0.2, 0.2 )
+ float z = RandomFloatRange( -0.2, 0.2 )
+
+ vector velocity = ( expect vector( attackParams.dir ) + Vector( x, y, z ) ) * 1500
+ vector angularVelocity = Vector( RandomFloatRange( -1200, 1200 ), 100, 0 )
+
+ bool hasIgnitionTime = vortexImpactWeaponInfo[ impactData.weaponName ].grenade_ignition_time > 0
+ float fuseTime = hasIgnitionTime ? 0.0 : baseFuseTime
+ const int HARDCODED_DAMAGE_TYPE = (damageTypes.explosive | DF_VORTEX_REFIRE)
+
+ entity grenade = vortexWeapon.FireWeaponGrenade( attackParams.pos, velocity, angularVelocity, fuseTime, HARDCODED_DAMAGE_TYPE, HARDCODED_DAMAGE_TYPE, PROJECTILE_NOT_PREDICTED, true, true )
+ if ( grenade )
+ {
+ Grenade_Init( grenade, vortexWeapon )
+ Vortex_ProjectileCommonSetup( grenade, impactData )
+ if ( hasIgnitionTime )
+ grenade.SetGrenadeIgnitionDuration( vortexImpactWeaponInfo[ impactData.weaponName ].grenade_ignition_time )
+ }
+
+ return (grenade ? true : false)
+}
+
+bool function DoVortexAttackForImpactData( entity vortexWeapon, attackParams, impactData, int attackSeedCount )
+{
+ bool didFire = false
+ switch ( impactData.refireBehavior )
+ {
+ case VORTEX_REFIRE_EXPLOSIVE_ROUND:
+ didFire = Vortex_FireBackExplosiveRound( vortexWeapon, attackParams, impactData, attackSeedCount )
+ break
+
+ case VORTEX_REFIRE_ROCKET:
+ didFire = Vortex_FireBackRocket( vortexWeapon, attackParams, impactData, attackSeedCount )
+ break
+
+ case VORTEX_REFIRE_GRENADE:
+ didFire = Vortex_FireBackGrenade( vortexWeapon, attackParams, impactData, attackSeedCount, 1.25 )
+ break
+
+ case VORTEX_REFIRE_GRENADE_LONG_FUSE:
+ didFire = Vortex_FireBackGrenade( vortexWeapon, attackParams, impactData, attackSeedCount, 10.0 )
+ break
+
+ case VORTEX_REFIRE_BULLET:
+ didFire = Vortex_FireBackProjectileBullet( vortexWeapon, attackParams, impactData, attackSeedCount )
+ break
+
+ case VORTEX_REFIRE_NONE:
+ break
+ }
+
+ return didFire
+}
+
+function Vortex_ProjectileCommonSetup( entity projectile, impactData )
+{
+ // custom tag it so it shows up correctly if it hits another vortex sphere
+ projectile.s.originalDamageSource <- impactData.damageSourceID
+
+ Vortex_SetImpactEffectTable_OnProjectile( projectile, impactData ) // set the correct impact effect table
+
+ projectile.SetVortexRefired( true ) // This tells code the projectile was refired from the vortex so that it uses "projectile_vortex_vscript"
+ projectile.SetModel( GetWeaponInfoFileKeyFieldAsset_Global( impactData.weaponName, "projectilemodel" ) )
+ projectile.SetWeaponClassName( impactData.weaponName ) // causes the projectile to use its normal trail FX
+
+ projectile.ProjectileSetDamageSourceID( impactData.damageSourceID ) // obit will show the owner weapon
+}
+
+// gives a refired projectile the correct impact effect table
+function Vortex_SetImpactEffectTable_OnProjectile( projectile, impactData )
+{
+ //Getting more info for bug 207595, don't check into Staging.
+ #if DEV
+ printt( "impactData.impact_effect_table ", impactData.impact_effect_table )
+ if ( impactData.impact_effect_table == "" )
+ PrintTable( impactData )
+ #endif
+
+ local fxTableHandle = GetImpactEffectTable( impactData.impact_effect_table )
+
+ projectile.SetImpactEffectTable( fxTableHandle )
+}
+#endif // SERVER
+
+// absorbed bullets are tracked with a special networked kv variable because clients need to know how many bullets to fire as well, when they are doing the client version of FireWeaponBullet
+int function GetBulletsAbsorbedCount( entity vortexWeapon )
+{
+ if ( !vortexWeapon )
+ return 0
+
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !vortexSphere )
+ return 0
+
+ return vortexSphere.GetBulletAbsorbedCount()
+}
+
+int function GetProjectilesAbsorbedCount( entity vortexWeapon )
+{
+ if ( !vortexWeapon )
+ return 0
+
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !vortexSphere )
+ return 0
+
+ return vortexSphere.GetProjectileAbsorbedCount()
+}
+
+#if SERVER
+function Vortex_GetProjectileImpacts( entity vortexWeapon )
+{
+ local impacts = []
+ foreach ( impactData in Vortex_GetAllImpactEvents( vortexWeapon ) )
+ {
+ if ( impactData.impactType == "projectile" )
+ impacts.append( impactData )
+ }
+
+ return impacts
+}
+
+function Vortex_GetHitscanBulletImpacts( entity vortexWeapon )
+{
+ local impacts = []
+ foreach ( impactData in Vortex_GetAllImpactEvents( vortexWeapon ) )
+ {
+ if ( impactData.impactType == "hitscan" )
+ impacts.append( impactData )
+ }
+
+ return impacts
+}
+
+int function GetHitscanBulletImpactCount( entity vortexWeapon )
+{
+ int count = 0
+ foreach ( impactData in Vortex_GetAllImpactEvents( vortexWeapon ) )
+ {
+ if ( impactData.impactType == "hitscan" )
+ count++
+ }
+
+ return count
+}
+#endif // SERVER
+
+// // lets the damage callback communicate to the attacker that he hit a vortex shield
+function Vortex_NotifyAttackerDidDamage( entity attacker, entity vortexOwner, hitPos )
+{
+ if ( !IsValid( attacker ) || !attacker.IsPlayer() )
+ return
+
+ if ( !IsValid( vortexOwner ) )
+ return
+
+ Assert( hitPos )
+
+ attacker.NotifyDidDamage( vortexOwner, 0, hitPos, 0, 0, DAMAGEFLAG_VICTIM_HAS_VORTEX, 0, null, 0 )
+}
+
+function SetVortexAmmo( entity vortexWeapon, count )
+{
+ entity owner = vortexWeapon.GetWeaponOwner()
+ if ( !IsValid_ThisFrame( owner ) )
+ return
+ #if CLIENT
+ if ( !IsLocalViewPlayer( owner ) )
+ return
+ #endif
+
+ vortexWeapon.SetWeaponPrimaryAmmoCount( count )
+}
+
+
+// sets the RGB color value for the vortex sphere FX based on current charge fraction
+function VortexSphereColorUpdate( entity weapon, sphereClientFXHandle = null )
+{
+ weapon.EndSignal( "VortexStopping" )
+
+ #if CLIENT
+ Assert( sphereClientFXHandle != null )
+ #endif
+ bool isIonVortex = weapon.GetWeaponClassName() == "mp_titanweapon_vortex_shield_ion"
+ entity weaponOwner = weapon.GetWeaponOwner()
+ float energyTotal = float ( weaponOwner.GetSharedEnergyTotal() )
+ while( IsValid( weapon ) && IsValid( weaponOwner ) )
+ {
+ vector colorVec
+ if ( isIonVortex )
+ {
+ float energyFrac = 1.0 - float( weaponOwner.GetSharedEnergyCount() ) / energyTotal
+ if ( weapon.HasMod( "pas_ion_vortex" ) )
+ colorVec = GetVortexSphereCurrentColor( energyFrac, VORTEX_SPHERE_COLOR_PAS_ION_VORTEX )
+ else
+ colorVec = GetVortexSphereCurrentColor( energyFrac )
+ }
+ else
+ {
+ colorVec = GetVortexSphereCurrentColor( weapon.GetWeaponChargeFraction() )
+ }
+
+
+ // update the world entity that is linked to the world FX playing on the server
+ #if SERVER
+ weapon.s.vortexSphereColorCP.SetOrigin( colorVec )
+ #else
+ // handles the server killing the vortex sphere without the client knowing right away,
+ // for example if an explosive goes off and we short circuit the charge timer
+ if ( !EffectDoesExist( sphereClientFXHandle ) )
+ break
+
+ EffectSetControlPointVector( sphereClientFXHandle, 1, colorVec )
+ #endif
+
+ WaitFrame()
+ }
+}
+
+vector function GetVortexSphereCurrentColor( float chargeFrac, vector fullHealthColor = VORTEX_SPHERE_COLOR_CHARGE_FULL )
+{
+ return GetTriLerpColor( chargeFrac, fullHealthColor, VORTEX_SPHERE_COLOR_CHARGE_MED, VORTEX_SPHERE_COLOR_CHARGE_EMPTY )
+}
+
+vector function GetShieldTriLerpColor( float frac )
+{
+ return GetTriLerpColor( frac, VORTEX_SPHERE_COLOR_CHARGE_FULL, VORTEX_SPHERE_COLOR_CHARGE_MED, VORTEX_SPHERE_COLOR_CHARGE_EMPTY )
+}
+
+vector function GetTriLerpColor( float fraction, vector color1, vector color2, vector color3 )
+{
+ float crossover1 = VORTEX_SPHERE_COLOR_CROSSOVERFRAC_FULL2MED // from zero to this fraction, fade between color1 and color2
+ float crossover2 = VORTEX_SPHERE_COLOR_CROSSOVERFRAC_MED2EMPTY // from crossover1 to this fraction, fade between color2 and color3
+
+ float r, g, b
+
+ // 0 = full charge, 1 = no charge remaining
+ if ( fraction < crossover1 )
+ {
+ r = Graph( fraction, 0, crossover1, color1.x, color2.x )
+ g = Graph( fraction, 0, crossover1, color1.y, color2.y )
+ b = Graph( fraction, 0, crossover1, color1.z, color2.z )
+ return <r, g, b>
+ }
+ else if ( fraction < crossover2 )
+ {
+ r = Graph( fraction, crossover1, crossover2, color2.x, color3.x )
+ g = Graph( fraction, crossover1, crossover2, color2.y, color3.y )
+ b = Graph( fraction, crossover1, crossover2, color2.z, color3.z )
+ return <r, g, b>
+ }
+ else
+ {
+ // for the last bit of overload timer, keep it max danger color
+ r = color3.x
+ g = color3.y
+ b = color3.z
+ return <r, g, b>
+ }
+
+ unreachable
+}
+
+// generic impact validation
+#if SERVER
+bool function ValidateVortexImpact( entity vortexSphere, entity projectile = null )
+{
+ Assert( IsServer() )
+
+ if ( !IsValid( vortexSphere ) )
+ return false
+
+ if ( !vortexSphere.GetOwnerWeapon() )
+ return false
+
+ entity vortexWeapon = vortexSphere.GetOwnerWeapon()
+ if ( !IsValid( vortexWeapon ) )
+ return false
+
+ if ( projectile )
+ {
+ if ( !IsValid_ThisFrame( projectile ) )
+ return false
+
+ if ( projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_ignores_vortex" ) == 1 )
+ return false
+
+ if ( projectile.ProjectileGetWeaponClassName() == "" )
+ return false
+
+ // TEMP HACK
+ if ( projectile.ProjectileGetWeaponClassName() == "mp_weapon_tether" )
+ return false
+ }
+
+ return true
+}
+#endif
+
+/********************************/
+/* Setting override functions */
+/********************************/
+
+function Vortex_SetTagName( entity weapon, string tagName )
+{
+ Vortex_SetWeaponSettingOverride( weapon, "vortexTagName", tagName )
+}
+
+function Vortex_SetBulletCollectionOffset( entity weapon, vector offset )
+{
+ Vortex_SetWeaponSettingOverride( weapon, "bulletCollectionOffset", offset )
+}
+
+function Vortex_SetWeaponSettingOverride( entity weapon, string setting, value )
+{
+ if ( !( setting in weapon.s ) )
+ weapon.s[ setting ] <- null
+ weapon.s[ setting ] = value
+}
+
+string function GetVortexTagName( entity weapon )
+{
+ if ( "vortexTagName" in weapon.s )
+ return expect string( weapon.s.vortexTagName )
+
+ return "vortex_center"
+}
+
+vector function GetBulletCollectionOffset( entity weapon )
+{
+ if ( "bulletCollectionOffset" in weapon.s )
+ return expect vector( weapon.s.bulletCollectionOffset )
+
+ entity owner = weapon.GetWeaponOwner()
+ if ( owner.IsTitan() )
+ return Vector( 300.0, -90.0, -70.0 )
+ else
+ return Vector( 80.0, 17.0, -11.0 )
+
+ unreachable
+}
+
+
+#if SERVER
+function VortexSphereDrainHealthForDamage( entity vortexSphere, damage )
+{
+ // don't drain the health of vortex_spheres that are set to be invulnerable. This is the case for the Particle Wall
+ if ( vortexSphere.IsInvulnerable() )
+ return
+
+ local result = {}
+ result.damage <- damage
+ vortexSphere.Signal( "Script_OnDamaged", result )
+
+ int currentHealth = vortexSphere.GetHealth()
+ Assert( damage >= 0 )
+ // JFS to fix phone home bug; we never hit the assert above locally...
+ damage = max( damage, 0 )
+ vortexSphere.SetHealth( currentHealth - damage )
+
+ entity vortexWeapon = vortexSphere.GetOwnerWeapon()
+ if ( IsValid( vortexWeapon ) && vortexWeapon.HasMod( "fd_gun_shield_redirect" ) )
+ {
+ entity owner = vortexWeapon.GetWeaponOwner()
+ if ( IsValid( owner ) && owner.IsTitan() )
+ {
+ entity soul = owner.GetTitanSoul()
+ if ( IsValid( soul ) )
+ {
+ int shieldRestoreAmount = int( damage ) //Might need tuning
+ soul.SetShieldHealth( min( soul.GetShieldHealth() + shieldRestoreAmount, soul.GetShieldHealthMax() ) )
+ }
+ }
+ }
+
+ UpdateShieldWallColorForFrac( vortexSphere.e.shieldWallFX, GetHealthFrac( vortexSphere ) )
+}
+#endif
+
+
+bool function CodeCallback_OnVortexHitBullet( entity weapon, entity vortexSphere, var damageInfo )
+{
+ bool isAmpedWall = vortexSphere.GetTargetName() == PROTO_AMPED_WALL
+ bool takesDamage = !isAmpedWall
+ bool adjustImpactAngles = !(vortexSphere.GetTargetName() == GUN_SHIELD_WALL)
+
+ #if SERVER
+ if ( vortexSphere.e.BulletHitRules != null )
+ {
+ vortexSphere.e.BulletHitRules( vortexSphere, damageInfo )
+ takesDamage = takesDamage && (DamageInfo_GetDamage( damageInfo ) > 0)
+ }
+ #endif
+
+ vector damageAngles = vortexSphere.GetAngles()
+
+ if ( adjustImpactAngles )
+ damageAngles = AnglesCompose( damageAngles, Vector( 90, 0, 0 ) )
+
+ int teamNum = vortexSphere.GetTeam()
+
+ #if CLIENT
+ vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo )
+ if ( !isAmpedWall )
+ {
+ // TODO: slightly change angles to match radius rotation of vortex cylinder
+ int effectHandle = StartParticleEffectInWorldWithHandle( GetParticleSystemIndex( SHIELD_WALL_BULLET_FX ), damageOrigin, damageAngles )
+ //local color = GetShieldTriLerpColor( 1 - GetHealthFrac( vortexSphere ) )
+ vector color = GetShieldTriLerpColor( 0.0 )
+ EffectSetControlPointVector( effectHandle, 1, color )
+ }
+
+ if ( takesDamage )
+ {
+ float damage = ceil( DamageInfo_GetDamage( damageInfo ) )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ DamageFlyout( damage, damageOrigin, vortexSphere, false, false )
+ }
+
+ if ( DamageInfo_GetAttacker( damageInfo ) && DamageInfo_GetAttacker( damageInfo ).IsTitan() )
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Heavy.BulletImpact_1P_vs_3P" )
+ else
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Light.BulletImpact_1P_vs_3P" )
+ #else
+ if ( !isAmpedWall )
+ {
+ int fxId = GetParticleSystemIndex( SHIELD_WALL_BULLET_FX )
+ PlayEffectOnVortexSphere( fxId, DamageInfo_GetDamagePosition( damageInfo ), damageAngles, vortexSphere )
+ }
+
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ float damage = ceil( DamageInfo_GetDamage( damageInfo ) )
+
+ Assert( damage >= 0, "Bug 159851 - Damage should be greater than or equal to 0.")
+ damage = max( 0.0, damage )
+
+ if ( IsValid( weapon ) )
+ damage = HandleWeakToPilotWeapons( vortexSphere, weapon.GetWeaponClassName(), damage )
+
+ if ( takesDamage )
+ {
+ //JFS - Arc Round bug fix for Monarch. Projectiles vortex callback doesn't even have damageInfo, so the shield modifier here doesn't exist in VortexSphereDrainHealthForDamage like it should.
+ ShieldDamageModifier damageModifier = GetShieldDamageModifier( damageInfo )
+ damage *= damageModifier.damageScale
+ VortexSphereDrainHealthForDamage( vortexSphere, damage )
+ }
+
+ if ( DamageInfo_GetAttacker( damageInfo ) && DamageInfo_GetAttacker( damageInfo ).IsTitan() )
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Heavy.BulletImpact_3P_vs_3P" )
+ else
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Light.BulletImpact_3P_vs_3P" )
+ #endif
+
+ if ( isAmpedWall )
+ {
+ #if SERVER
+ DamageInfo_ScaleDamage( damageInfo, AMPED_DAMAGE_SCALAR )
+ #endif
+ return false
+ }
+
+ return true
+}
+
+bool function OnVortexHitBullet_BubbleShieldNPC( entity vortexSphere, var damageInfo )
+{
+ vector vortexOrigin = vortexSphere.GetOrigin()
+ vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo )
+
+ float distSq = DistanceSqr( vortexOrigin, damageOrigin )
+ if ( distSq < MINION_BUBBLE_SHIELD_RADIUS_SQR )
+ return false//the damage is coming from INSIDE the sphere
+
+ vector damageVec = damageOrigin - vortexOrigin
+ vector damageAngles = VectorToAngles( damageVec )
+ damageAngles = AnglesCompose( damageAngles, Vector( 90, 0, 0 ) )
+
+ int teamNum = vortexSphere.GetTeam()
+
+ #if CLIENT
+ int effectHandle = StartParticleEffectInWorldWithHandle( GetParticleSystemIndex( SHIELD_WALL_BULLET_FX ), damageOrigin, damageAngles )
+
+ vector color = GetShieldTriLerpColor( 0.9 )
+ EffectSetControlPointVector( effectHandle, 1, color )
+
+ if ( DamageInfo_GetAttacker( damageInfo ) && DamageInfo_GetAttacker( damageInfo ).IsTitan() )
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Heavy.BulletImpact_1P_vs_3P" )
+ else
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Light.BulletImpact_1P_vs_3P" )
+ #else
+ int fxId = GetParticleSystemIndex( SHIELD_WALL_BULLET_FX )
+ PlayEffectOnVortexSphere( fxId, DamageInfo_GetDamagePosition( damageInfo ), damageAngles, vortexSphere )
+ //VortexSphereDrainHealthForDamage( vortexSphere, DamageInfo_GetWeapon( damageInfo ), null )
+
+ if ( DamageInfo_GetAttacker( damageInfo ) && DamageInfo_GetAttacker( damageInfo ).IsTitan() )
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Heavy.BulletImpact_3P_vs_3P" )
+ else
+ EmitSoundAtPosition( teamNum, DamageInfo_GetDamagePosition( damageInfo ), "TitanShieldWall.Light.BulletImpact_3P_vs_3P" )
+ #endif
+ return true
+}
+
+bool function CodeCallback_OnVortexHitProjectile( entity weapon, entity vortexSphere, entity attacker, entity projectile, vector contactPos )
+{
+ // code shouldn't call this on an invalid vortexsphere!
+ if ( !IsValid( vortexSphere ) )
+ return false
+
+ var ignoreVortex = projectile.ProjectileGetWeaponInfoFileKeyField( "projectile_ignores_vortex" )
+ if ( ignoreVortex != null )
+ {
+ #if SERVER
+ if ( projectile.proj.hasBouncedOffVortex )
+ return false
+
+ vector velocity = projectile.GetVelocity()
+ vector multiplier
+
+ switch ( ignoreVortex )
+ {
+ case "drop":
+ multiplier = < -0.25, -0.25, 0.0 >
+ break
+
+ case "fall_vortex":
+ case "fall":
+ multiplier = < -0.25, -0.25, -0.25 >
+ break
+
+ case "mirror":
+ // bounce back, assume along xy axis
+ multiplier = < -1.0, -1.0, 1.0 >
+ break
+
+ default:
+ CodeWarning( "Unknown projectile_ignores_vortex " + ignoreVortex )
+ break
+ }
+
+ velocity = < velocity.x * multiplier.x, velocity.y * multiplier.y, velocity.z * multiplier.z >
+ projectile.proj.hasBouncedOffVortex = true
+ projectile.SetVelocity( velocity )
+ #endif
+ return false
+ }
+
+ bool adjustImpactAngles = !(vortexSphere.GetTargetName() == GUN_SHIELD_WALL)
+
+ vector damageAngles = vortexSphere.GetAngles()
+
+ if ( adjustImpactAngles )
+ damageAngles = AnglesCompose( damageAngles, Vector( 90, 0, 0 ) )
+
+ asset projectileSettingFX = projectile.GetProjectileWeaponSettingAsset( eWeaponVar.vortex_impact_effect )
+ asset impactFX = (projectileSettingFX != $"") ? projectileSettingFX : SHIELD_WALL_EXPMED_FX
+
+ bool isAmpedWall = vortexSphere.GetTargetName() == PROTO_AMPED_WALL
+ bool takesDamage = !isAmpedWall
+
+ #if SERVER
+ if ( vortexSphere.e.ProjectileHitRules != null )
+ takesDamage = vortexSphere.e.ProjectileHitRules( vortexSphere, attacker, takesDamage )
+ #endif
+ // hack to let client know about amped wall, and to amp the shot
+ if ( isAmpedWall )
+ impactFX = AMPED_WALL_IMPACT_FX
+
+ int teamNum = vortexSphere.GetTeam()
+
+ #if CLIENT
+ if ( !isAmpedWall )
+ {
+ int effectHandle = StartParticleEffectInWorldWithHandle( GetParticleSystemIndex( impactFX ), contactPos, damageAngles )
+ //local color = GetShieldTriLerpColor( 1 - GetHealthFrac( vortexSphere ) )
+ vector color = GetShieldTriLerpColor( 0.0 )
+ EffectSetControlPointVector( effectHandle, 1, color )
+ }
+
+ var impact_sound_1p = projectile.ProjectileGetWeaponInfoFileKeyField( "vortex_impact_sound_1p" )
+ if ( impact_sound_1p == null )
+ impact_sound_1p = "TitanShieldWall.Explosive.BulletImpact_1P_vs_3P"
+
+ EmitSoundAtPosition( teamNum, contactPos, impact_sound_1p )
+ #else
+ if ( !isAmpedWall )
+ {
+ int fxId = GetParticleSystemIndex( impactFX )
+ PlayEffectOnVortexSphere( fxId, contactPos, damageAngles, vortexSphere )
+ }
+
+ float damage = float( projectile.GetProjectileWeaponSettingInt( eWeaponVar.damage_near_value ) )
+ // once damageInfo is passed correctly we'll use that instead of looking up the values from the weapon .txt file.
+ // local damage = ceil( DamageInfo_GetDamage( damageInfo ) )
+
+ damage = HandleWeakToPilotWeapons( vortexSphere, projectile.ProjectileGetWeaponClassName(), damage )
+ damage = damage + CalculateTitanSniperExtraDamage( projectile, vortexSphere )
+
+ if ( takesDamage )
+ {
+ VortexSphereDrainHealthForDamage( vortexSphere, damage )
+ if ( IsValid( attacker ) && attacker.IsPlayer() )
+ attacker.NotifyDidDamage( vortexSphere, 0, contactPos, 0, damage, DF_NO_HITBEEP, 0, null, 0 )
+ }
+
+ var impact_sound_3p = projectile.ProjectileGetWeaponInfoFileKeyField( "vortex_impact_sound_3p" )
+
+ if ( impact_sound_3p == null )
+ impact_sound_3p = "TitanShieldWall.Explosive.BulletImpact_3P_vs_3P"
+
+ EmitSoundAtPosition( teamNum, contactPos, impact_sound_3p )
+
+ int damageSourceID = projectile.ProjectileGetDamageSourceID()
+ switch ( damageSourceID )
+ {
+ case eDamageSourceId.mp_titanweapon_dumbfire_rockets:
+ vector normal = projectile.GetVelocity() * -1
+ normal = Normalize( normal )
+ ClusterRocket_Detonate( projectile, normal )
+ CreateNoSpawnArea( TEAM_INVALID, TEAM_INVALID, contactPos, ( CLUSTER_ROCKET_BURST_COUNT / 5.0 ) * 0.5 + 1.0, CLUSTER_ROCKET_BURST_RANGE + 100 )
+ break
+
+ case eDamageSourceId.mp_weapon_grenade_electric_smoke:
+ ElectricGrenadeSmokescreen( projectile, FX_ELECTRIC_SMOKESCREEN_PILOT_AIR )
+ break
+
+ case eDamageSourceId.mp_weapon_grenade_emp:
+
+ if ( StatusEffect_Get( vortexSphere, eStatusEffect.destroyed_by_emp ) )
+ VortexSphereDrainHealthForDamage( vortexSphere, vortexSphere.GetHealth() )
+ break
+
+ case eDamageSourceId.mp_titanability_sonar_pulse:
+ if ( IsValid( attacker ) && attacker.IsTitan() )
+ {
+ int team = attacker.GetTeam()
+ PulseLocation( attacker, team, contactPos, false, false )
+ array<string> mods = projectile.ProjectileGetMods()
+ if ( mods.contains( "pas_tone_sonar" ) )
+ thread DelayedPulseLocation( attacker, team, contactPos, false, false )
+ }
+ break
+
+ }
+ #endif
+
+ // hack to let client know about amped wall, and to amp the shot
+ if ( isAmpedWall )
+ {
+ #if SERVER
+ projectile.proj.damageScale = AMPED_DAMAGE_SCALAR
+ #endif
+
+ return false
+ }
+
+ return true
+}
+
+bool function OnVortexHitProjectile_BubbleShieldNPC( entity vortexSphere, entity attacker, entity projectile, vector contactPos )
+{
+ vector vortexOrigin = vortexSphere.GetOrigin()
+
+ float dist = DistanceSqr( vortexOrigin, contactPos )
+ if ( dist < MINION_BUBBLE_SHIELD_RADIUS_SQR )
+ return false // the damage is coming from INSIDE THE SPHERE
+
+ vector damageVec = Normalize( contactPos - vortexOrigin )
+ vector damageAngles = VectorToAngles( damageVec )
+ damageAngles = AnglesCompose( damageAngles, Vector( 90, 0, 0 ) )
+
+ asset projectileSettingFX = projectile.GetProjectileWeaponSettingAsset( eWeaponVar.vortex_impact_effect )
+ asset impactFX = (projectileSettingFX != $"") ? projectileSettingFX : SHIELD_WALL_EXPMED_FX
+
+ int teamNum = vortexSphere.GetTeam()
+
+ #if CLIENT
+ int effectHandle = StartParticleEffectInWorldWithHandle( GetParticleSystemIndex( impactFX ), contactPos, damageAngles )
+
+ vector color = GetShieldTriLerpColor( 0.9 )
+ EffectSetControlPointVector( effectHandle, 1, color )
+
+ EmitSoundAtPosition( teamNum, contactPos, "TitanShieldWall.Explosive.BulletImpact_1P_vs_3P" )
+ #else
+ int fxId = GetParticleSystemIndex( impactFX )
+ PlayEffectOnVortexSphere( fxId, contactPos, damageAngles, vortexSphere )
+// VortexSphereDrainHealthForDamage( vortexSphere, null, projectile )
+
+ EmitSoundAtPosition( teamNum, contactPos, "TitanShieldWall.Explosive.BulletImpact_3P_vs_3P" )
+
+ if ( projectile.ProjectileGetDamageSourceID() == eDamageSourceId.mp_titanweapon_dumbfire_rockets )
+ {
+ vector normal = projectile.GetVelocity() * -1
+ normal = Normalize( normal )
+ ClusterRocket_Detonate( projectile, normal )
+ CreateNoSpawnArea( TEAM_INVALID, TEAM_INVALID, contactPos, ( CLUSTER_ROCKET_BURST_COUNT / 5.0 ) * 0.5 + 1.0, CLUSTER_ROCKET_BURST_RANGE + 100 )
+ }
+ #endif
+ return true
+}
+
+#if SERVER
+float function HandleWeakToPilotWeapons( entity vortexSphere, string weaponName, float damage )
+{
+ if ( vortexSphere.e.proto_weakToPilotWeapons ) //needs code for real, but this is fine for prototyping
+ {
+ // is weapon a pilot weapon?
+ local refType = GetWeaponInfoFileKeyField_Global( weaponName, "weaponClass" )
+ if ( refType == "human" )
+ {
+ damage *= VORTEX_PILOT_WEAPON_WEAKNESS_DAMAGESCALE
+ }
+ }
+
+ return damage
+}
+#endif
+
+// ???: reflectOrigin not used
+int function VortexReflectAttack( entity vortexWeapon, attackParams, vector reflectOrigin )
+{
+ entity vortexSphere = vortexWeapon.GetWeaponUtilityEntity()
+ if ( !vortexSphere )
+ return 0
+
+ #if SERVER
+ Assert( vortexSphere )
+ #endif
+
+ int totalfired = 0
+ int totalAttempts = 0
+
+ bool forceReleased = false
+ // in this case, it's also considered "force released" if the charge time runs out
+ if ( vortexWeapon.IsForceRelease() || vortexWeapon.GetWeaponChargeFraction() == 1 )
+ forceReleased = true
+
+ //Requires code feature to properly fire tracers from offset positions.
+ //if ( vortexWeapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ // attackParams.pos = reflectOrigin
+
+ // PREDICTED REFIRES
+ // bullet impact events don't individually fire back per event because we aggregate and then shotgun blast them
+
+ //Remove the below script after FireWeaponBulletBroadcast
+ //local bulletsFired = Vortex_FireBackBullets( vortexWeapon, attackParams )
+ //totalfired += bulletsFired
+ int bulletCount = GetBulletsAbsorbedCount( vortexWeapon )
+ if ( bulletCount > 0 )
+ {
+ if ( "ampedBulletCount" in vortexWeapon.s )
+ vortexWeapon.s.ampedBulletCount++
+ else
+ vortexWeapon.s.ampedBulletCount <- 1
+ vortexWeapon.Signal( "FireAmpedVortexBullet" )
+ totalfired += 1
+ }
+
+ // UNPREDICTED REFIRES
+ #if SERVER
+ //printt( "server: force released?", forceReleased )
+
+ local unpredictedRefires = Vortex_GetProjectileImpacts( vortexWeapon )
+
+ // HACK we don't actually want to refire them with a spiral but
+ // this is to temporarily ensure compatibility with the Titan rocket launcher
+ if ( !( "spiralMissileIdx" in vortexWeapon.s ) )
+ vortexWeapon.s.spiralMissileIdx <- null
+ vortexWeapon.s.spiralMissileIdx = 0
+ foreach ( impactData in unpredictedRefires )
+ {
+ bool didFire = DoVortexAttackForImpactData( vortexWeapon, attackParams, impactData, totalAttempts )
+ if ( didFire )
+ totalfired++
+ totalAttempts++
+ }
+ #endif
+
+ SetVortexAmmo( vortexWeapon, 0 )
+ vortexWeapon.Signal( "VortexFired" )
+
+#if SERVER
+ vortexSphere.ClearAllBulletsFromSphere()
+#endif
+
+ /*
+ if ( forceReleased )
+ DestroyVortexSphereFromVortexWeapon( vortexWeapon )
+ else
+ DisableVortexSphereFromVortexWeapon( vortexWeapon )
+ */
+
+ return totalfired
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/weapons/_weapon_dialogue.nut b/Northstar.CustomServers/scripts/vscripts/weapons/_weapon_dialogue.nut
new file mode 100644
index 00000000..04fd24d3
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/weapons/_weapon_dialogue.nut
@@ -0,0 +1,44 @@
+untyped
+
+globalize_all_functions
+
+function Weapon_Dialogue_Init()
+{
+ RegisterConversation( "CoopTD_TurretAvailable", VO_PRIORITY_PLAYERSTATE ) // player turret becomes available
+ RegisterConversation( "CoopTD_TurretAvailableNag", VO_PRIORITY_PLAYERSTATE ) // player turret available nag
+ RegisterConversation( "CoopTD_TurretDestroyed", VO_PRIORITY_PLAYERSTATE ) // player turret destroyed
+ RegisterConversation( "CoopTD_TurretDeadAndReady", VO_PRIORITY_PLAYERSTATE ) // player turret destroyed, and one ready
+ RegisterConversation( "CoopTD_TurretKillstreak", VO_PRIORITY_PLAYERSTATE ) // player turret has lots of kills
+ RegisterConversation( "CoopTD_TurretKilledTitan", VO_PRIORITY_PLAYERSTATE ) // player turret killed a titan
+ RegisterConversation( "CoopTD_TurretKilledTitan_Multi", VO_PRIORITY_PLAYERSTATE ) // player turret killed multiple titans
+
+ #if CLIENT
+
+ AddVDULineForSarah( "CoopTD_TurretAvailable", "diag_gm_coop_playerTurretEarned_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretAvailable", "diag_gm_coop_playerTurretEarned_mcor_Sarah" )
+
+ // player turret available nag
+ AddVDULineForSarah( "CoopTD_TurretAvailableNag", "diag_gm_coop_playerTurretNag_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretAvailableNag", "diag_gm_coop_playerTurretNag_mcor_Sarah" )
+
+ // player turret destroyed
+ AddVDULineForSarah( "CoopTD_TurretDestroyed", "diag_gm_coop_playerTurretDestro_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretDestroyed", "diag_gm_coop_playerTurretDestro_mcor_Sarah" )
+
+ // player turret destroyed and another one ready
+ AddVDULineForSarah( "CoopTD_TurretDeadAndReady", "diag_gm_coop_playerTurretDeadAndReady_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretDeadAndReady", "diag_gm_coop_playerTurretDeadAndReady_mcor_Sarah" )
+
+ // player turret has lots of kills
+ AddVDULineForSarah( "CoopTD_TurretKillstreak", "diag_gm_coop_playerTurretHighKills_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretKillstreak", "diag_gm_coop_playerTurretHighKills_mcor_Sarah" )
+
+ // player turret killed a titan
+ AddVDULineForSarah( "CoopTD_TurretKilledTitan", "diag_gm_coop_playerTurretKilledTitan_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretKilledTitan", "diag_gm_coop_playerTurretKilledTitan_mcor_Sarah" )
+
+ // player turret killed multiple titans
+ AddVDULineForSarah( "CoopTD_TurretKilledTitan_Multi", "diag_gm_coop_playerTurretKilledTitanAgain_mcor_Sarah" )
+ AddVDULineForSpyglass( "CoopTD_TurretKilledTitan_Multi", "diag_gm_coop_playerTurretKilledTitanAgain_mcor_Sarah" )
+ #endif
+}
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 00000000..b3e5f5a3
--- /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<ColorSwapStruct> 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<entity> 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<VisibleEntityInCone> 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<entity> 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<entity> 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<entity> 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<entity> 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<entity> 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<entity> 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<entity> 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<entity> 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<string> function GetWeaponBurnMods( string weaponClassName )
+{
+ array<string> burnMods = []
+ array<string> 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<string> 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<entity> traps = GetScriptManagedEntArray( owner.s.activeTrapArrayId )
+ array<entity> 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<entity> 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<entity> 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<entity> guys = GetPlayerArrayEx( "any", TEAM_ANY, team, origin, radius + worldSpaceCenterBuffer )
+ foreach ( guy in guys )
+ {
+ if ( IsAlive( guy ) && Distance( origin, guy.GetWorldSpaceCenter() ) < radius )
+ return true
+ }
+
+ array<entity> 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<float> 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<entity> function GetPrimaryWeapons( entity player )
+{
+ array<entity> primaryWeapons
+ array<entity> 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<entity> function GetSidearmWeapons( entity player )
+{
+ array<entity> sidearmWeapons
+ array<entity> weapons = player.GetMainWeapons()
+ foreach ( weaponEnt in weapons )
+ {
+ if ( weaponEnt.GetWeaponType() != WT_SIDEARM )
+ continue
+
+ sidearmWeapons.append( weaponEnt )
+ }
+ return sidearmWeapons
+}
+
+array<entity> function GetATWeapons( entity player )
+{
+ array<entity> atWeapons
+ array<entity> 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<entity> 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<string> 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<string> mods = [] )
+{
+ player.TakeOffhandWeapon( OFFHAND_SPECIAL )
+ player.GiveOffhandWeapon( offhandName, OFFHAND_SPECIAL, mods )
+}
+
+void function ReplacePlayerOrdnance( entity player, string ordnanceName, array<string> 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<string> 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<string> excludeNames )
+{
+ array<entity> weapons = GetPlayerWeapons( player, excludeNames )
+ foreach ( weapon in weapons )
+ weapon.AllowUse( false )
+}
+
+void function EnableWeapons( entity player, array<string> excludeNames )
+{
+ array<entity> weapons = GetPlayerWeapons( player, excludeNames )
+ foreach ( weapon in weapons )
+ weapon.AllowUse( true )
+}
+
+array<entity> function GetPlayerWeapons( entity player, array<string> excludeNames )
+{
+ array<entity> 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<vector> positions = []
+ vector lastDownPos
+ bool firstTrace = true
+
+ dir = <dir.x, dir.y, 0.0>
+ 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 = <newPos.x, newPos.y, traceStart.z - 40.0 >
+ traceEndOver = <newPos.x, newPos.y, traceStart.z + step * 0.57735056839> // 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<entity> 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<string> 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<string> 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<int> 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<int> 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<int> 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<int> 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