aboutsummaryrefslogtreecommitdiff
path: root/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut
diff options
context:
space:
mode:
authorGeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com>2024-08-14 17:31:06 +0200
committerGitHub <noreply@github.com>2024-08-14 17:31:06 +0200
commitfa4e319c0b60cd68a3dccaa4322e3c35cfa1e385 (patch)
tree4a78391c5008a4aa50d81fbca5d9f26bb475cd96 /Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut
parentbcec5a5e9edd2a2af3a017ea4b250a9ba1112e6f (diff)
parent7aa3958ccd8e32970736654dfae0c7a87f0798bb (diff)
downloadNorthstarMods-fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385.tar.gz
NorthstarMods-fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385.zip
Merge branch 'main' into permanent-amped-weapons-fix-prpermanent-amped-weapons-fix-pr
Diffstat (limited to 'Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut')
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut1033
1 files changed, 1033 insertions, 0 deletions
diff --git a/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut b/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut
new file mode 100644
index 00000000..defb1a56
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut
@@ -0,0 +1,1033 @@
+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 = IsValid( vortexWeapon ) ? vortexWeapon.GetWeaponClassName() : ""
+ if ( vortexWeapon && ( className == "mp_titanweapon_vortex_shield" || className == "mp_titanweapon_vortex_shield_ion" ) )
+ {
+ float amount = expect float ( chargeFrac ) * 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 )
+ }
+ }
+ 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, 1, 0.0, 1.0 ) * 10 // do more damage the more charged the weapon is.
+ VortexSphereDrainHealthForDamage( vortexHit.vortex, damage )
+ if ( IsValid( player ) && player.IsPlayer() )
+ player.NotifyDidDamage( vortexHit.vortex, 0, vortexHit.hitPos, 0, damage, DF_NO_HITBEEP, 0, null, 0 )
+ }
+ #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 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
+ int damageFarDistance = eWeaponVar.damage_far_distance
+ int damageNearDistance = eWeaponVar.damage_near_distance
+ 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
+ damageFarDistance = eWeaponVar.npc_damage_far_distance
+ damageNearDistance = eWeaponVar.npc_damage_near_distance
+ }
+
+ if ( IsValid( target ) && IsValid( zapInfo.player ) )
+ {
+ bool hasFastPacitor = false
+ bool noArcing = false
+
+ entity weapon = expect entity( zapInfo.weapon )
+ hasFastPacitor = weapon.GetWeaponInfoFileKeyField( "push_apart" ) != null && weapon.GetWeaponInfoFileKeyField( "push_apart" ) == 1
+ noArcing = weapon.GetWeaponInfoFileKeyField( "no_arcing" ) != null && weapon.GetWeaponInfoFileKeyField( "no_arcing" ) == 1
+ float critScale = weapon.GetWeaponSettingFloat( eWeaponVar.critical_hit_damage_scale )
+
+ if ( target.GetArmorType() == ARMOR_TYPE_HEAVY )
+ {
+ damageMin = weapon.GetWeaponSettingInt( damageFarValueTitanArmor )
+ damageMax = weapon.GetWeaponSettingInt( damageNearValueTitanArmor )
+ }
+ else
+ {
+ damageMin = weapon.GetWeaponSettingInt( damageFarValue )
+ damageMax = weapon.GetWeaponSettingInt( damageNearValue )
+
+ if ( target.IsNPC() )
+ {
+ damageMin *= 3 // more powerful against NPC humans so they die easy
+ damageMax *= 3
+ }
+ }
+
+
+ local chargeRatio = GetArcCannonChargeFraction( weapon )
+ if ( !weapon.GetWeaponSettingBool( eWeaponVar.charge_require_input ) )
+ {
+ // use distance for damage if the weapon auto-fires
+ float nearDist = weapon.GetWeaponSettingFloat( damageNearDistance )
+ float farDist = weapon.GetWeaponSettingFloat( damageFarDistance )
+
+ 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 ( weapon.HasMod( "splitter" ) )
+ damageFalloff = SPLITTER_DAMAGE_FALLOFF_SCALER
+ damageAmount *= pow( damageFalloff, chainNum - 1 )
+
+ local isMissile = ( target.GetClassName() == "rpg_missile" )
+ if ( !isMissile )
+ wait ARC_CANNON_FORK_DELAY
+ else
+ wait 0.05
+
+ if ( !IsValid( target ) || !IsValid( zapInfo.player ) )
+ return
+
+ 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,criticalHitScale = critScale } )
+ //vector dir = Normalize( beamEndPos - beamStartPos )
+ //vector velocity = dir * 600
+ //PushPlayerAway( target, velocity )
+ //PushPlayerAway( expect entity( zapInfo.player ), -velocity )
+
+ if ( IsValid( 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, entity weapon )
+{
+ array<entity> targets = GetScriptManagedEntArrayWithinCenter( level._arcCannonTargetsArrayID, team, origin, ARC_CANNON_TITAN_RANGE_CHAIN )
+
+ if ( ARC_CANNON_TARGETS_MISSILES && weapon.GetWeaponChargeFraction() == 1.0 )
+ 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, weapon )
+ 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
+}