diff options
author | Respawn <respawn@northstar.tf> | 2023-07-16 22:58:48 +0200 |
---|---|---|
committer | Respawn <respawn@northstar.tf> | 2023-07-16 22:58:48 +0200 |
commit | ef596fb867c91e4772bdc87e51ca7ecafc9217ee (patch) | |
tree | 79c12911014934f9fdaa2f9fe5b5e7f141e1cc6f | |
parent | bceb950a18ee0a8155acb99bb66dd57e4107547e (diff) | |
download | NorthstarMods-ef596fb867c91e4772bdc87e51ca7ecafc9217ee.tar.gz NorthstarMods-ef596fb867c91e4772bdc87e51ca7ecafc9217ee.zip |
Add _arc_cannon.nut from englishclient_mp_common
-rw-r--r-- | Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut | 1006 |
1 files changed, 1006 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..4268422e --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut @@ -0,0 +1,1006 @@ + +// Aiming & Range +const DEFAULT_ARC_CANNON_FOVDOT = 0.98 // First target must be within this dot to be zapped and start a chain +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 ) +const ARC_CANNON_RANGE_CHAIN = 400 // Max distance we can arc from one target to another +const ARC_CANNON_TITAN_RANGE_CHAIN = 900 // Max distance we can arc from one target to another +const ARC_CANNON_CHAIN_COUNT_MIN = 5 // Max number of chains at no charge +const ARC_CANNON_CHAIN_COUNT_MAX = 5 // Max number of chains at full charge +const ARC_CANNON_CHAIN_COUNT_NPC = 2 // Number of chains when an NPC fires the weapon +const ARC_CANNON_FORK_COUNT_MAX = 1 // Number of forks that can come out of one target to other targets +const ARC_CANNON_FORK_DELAY = 0.1 + +const ARC_CANNON_RANGE_CHAIN_BURN = 400 +const ARC_CANNON_TITAN_RANGE_CHAIN_BURN = 900 +const ARC_CANNON_CHAIN_COUNT_MIN_BURN = 100 // Max number of chains at no charge +const ARC_CANNON_CHAIN_COUNT_MAX_BURN = 100 // Max number of chains at full charge +const ARC_CANNON_CHAIN_COUNT_NPC_BURN = 10 // Number of chains when an NPC fires the weapon +const ARC_CANNON_FORK_COUNT_MAX_BURN = 10 // Number of forks that can come out of one target to other targets +const ARC_CANNON_BEAM_LIFETIME_BURN = 1 + +// Visual settings +const ARC_CANNON_BOLT_RADIUS_MIN = 32 // Bolt radius at no charge ( not actually sure what this does to the beam lol ) +const ARC_CANNON_BOLT_RADIUS_MAX = 640 // Bold radius at full charge ( not actually sure what this does to the beam lol ) +const ARC_CANNON_BOLT_WIDTH_MIN = 1 // Bolt width at no charge +const ARC_CANNON_BOLT_WIDTH_MAX = 26 // Bolt width at full charge +const ARC_CANNON_BOLT_WIDTH_NPC = 8 // Bolt width when used by NPC +const ARC_CANNON_BEAM_COLOR = "150 190 255" +const ARC_CANNON_BEAM_LIFETIME = 0.75 + +// Player Effects +const ARC_CANNON_TITAN_SCREEN_SFX = "Weapon_R1_LaserMine.Activate" +const ARC_CANNON_PILOT_SCREEN_SFX = "Weapon_R1_LaserMine.Activate" +const ARC_CANNON_EMP_DURATION_MIN = 0.1 +const ARC_CANNON_EMP_DURATION_MAX = 1.8 +const ARC_CANNON_EMP_FADEOUT_DURATION = 0.4 +const ARC_CANNON_SCREEN_EFFECTS_MIN = 0.025 +const ARC_CANNON_SCREEN_EFFECTS_MAX = 0.075 +const ARC_CANNON_SCREEN_THRESHOLD = 0.3385 +const ARC_CANNON_SLOW_SCALE_MIN = 0.8 +const ARC_CANNON_SLOW_SCALE_MAX = 0.7 +const ARC_CANNON_3RD_PERSON_EFFECT_MIN_DURATION = 0.2 + +// Rumble +const ARC_CANNON_RUMBLE_CHARGE_MIN = 5 +const ARC_CANNON_RUMBLE_CHARGE_MAX = 50 +const ARC_CANNON_RUMBLE_TYPE_INDEX = 14 // These are defined in code, 14 = RUMBLE_FLAT_BOTH + +// Damage +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 +const ARC_CANNON_DAMAGE_CHARGE_RATIO = 0.85 // What amount of charge is required for full damage. +const ARC_CANNON_DAMAGE_CHARGE_RATIO_BURN = 0.676 // What amount of charge is required for full damage. +const ARC_CANNON_CAPACITOR_CHARGE_RATIO = 1.0 + +// Options +const ARC_CANNON_TARGETS_MISSILES = 1 // 1 = arc cannon zaps missiles that are active, 0 = missiles are ignored by arc cannon + +//Mods +const OVERCHARGE_MAX_SHIELD_DECAY = 0.2 +const OVERCHARGE_SHIELD_DECAY_MULTIPLIER = 0.04 +const OVERCHARGE_BONUS_CHARGE_FRACTION = 0.05 + +const SPLITTER_DAMAGE_FALLOFF_SCALER = 0.6 +const SPLITTER_FORK_COUNT_MAX = 10 + +const ARC_CANNON_SIGNAL_DEACTIVATED = "ArcCannonDeactivated" +RegisterSignal( ARC_CANNON_SIGNAL_DEACTIVATED ) + +const ARC_CANNON_SIGNAL_CHARGEEND = "ArcCannonChargeEnd" +RegisterSignal( ARC_CANNON_SIGNAL_CHARGEEND ) + +const ARC_CANNON_BEAM_EFFECT = "wpn_arc_cannon_beam" +PrecacheParticleSystem( ARC_CANNON_BEAM_EFFECT ) + +const ARC_CANNON_BEAM_EFFECT_MOD = "wpn_arc_cannon_beam_mod" +PrecacheParticleSystem( ARC_CANNON_BEAM_EFFECT_MOD ) + +const ARC_CANNON_FX_TABLE = "exp_arc_cannon" +PrecacheImpactEffectTable( ARC_CANNON_FX_TABLE ) + +if ( !reloadingScripts ) +{ + // Valid Arc Cannon Target Classnames + level.arcCannonTargetClassnames <- {} + level.arcCannonTargetClassnames[ "npc_turret_floor" ] <- true + level.arcCannonTargetClassnames[ "npc_spectre" ] <- true + level.arcCannonTargetClassnames[ "npc_soldier_shield" ] <- true + level.arcCannonTargetClassnames[ "npc_soldier_heavy" ] <- true + level.arcCannonTargetClassnames[ "npc_soldier" ] <- true + level.arcCannonTargetClassnames[ "npc_cscanner" ] <- true + level.arcCannonTargetClassnames[ "npc_titan" ] <- true + level.arcCannonTargetClassnames[ "npc_marvin" ] <- true + level.arcCannonTargetClassnames[ "player" ] <- true + level.arcCannonTargetClassnames[ "script_mover" ] <- true + level.arcCannonTargetClassnames[ "npc_grenade_frag" ] <- true + level.arcCannonTargetClassnames[ "rpg_missile" ] <- true + level.arcCannonTargetClassnames[ "npc_turret_mega" ] <- true + level.arcCannonTargetClassnames[ "npc_turret_sentry" ] <- true + level.arcCannonTargetClassnames[ "npc_dropship" ] <- true + level.arcCannonTargetClassnames[ "prop_dynamic" ] <- true +} + +function main() +{ + Globalize( ArcCannon_PrecacheFX ) + Globalize( ArcCannon_Start ) + Globalize( ArcCannon_Stop ) + Globalize( ArcCannon_ChargeBegin ) + Globalize( ArcCannon_ChargeEnd ) + Globalize( FireArcCannon ) + Globalize( ArcCannon_HideIdleEffect ) + Globalize( AddToArcCannonTargets ) + Globalize( ConvertTitanShieldIntoBonusCharge ) + Globalize( GetArcCannonChargeFraction ) + Globalize( StopChargeEffects ) + + if( IsClient() ) + { + AddDestroyCallback( "mp_titanweapon_arc_cannon", ClientDestroyCallback_ArcCannon_Stop ) + } + else + { + level._arcCannonTargetsArrayID <- CreateScriptManagedEntArray() + } + + PrecacheParticleSystem( "impact_arc_cannon_titan" ) +} + +function ArcCannon_PrecacheFX( weapon ) +{ + if ( WeaponIsPrecached( weapon ) ) + return + + PrecacheParticleSystem( "wpn_arc_cannon_electricity_fp" ) + PrecacheParticleSystem( "wpn_arc_cannon_electricity" ) + + PrecacheParticleSystem( "wpn_ARC_knob_FP" ) + PrecacheParticleSystem( "wpn_ARC_knob" ) + + PrecacheParticleSystem( "wpn_arc_cannon_charge_fp" ) + PrecacheParticleSystem( "wpn_arc_cannon_charge" ) + + PrecacheParticleSystem( "wpn_arc_cannon_charge_fp" ) + PrecacheParticleSystem( "wpn_arc_cannon_charge" ) + + PrecacheParticleSystem( "wpn_muzzleflash_arc_cannon_fp" ) + PrecacheParticleSystem( "wpn_muzzleflash_arc_cannon" ) +} + +function ArcCannon_Start( weapon ) +{ + weapon.PlayWeaponEffectNoCull( "wpn_arc_cannon_electricity_fp", "wpn_arc_cannon_electricity", "muzzle_flash" ) + weapon.EmitWeaponSound( "arc_cannon_charged_loop" ) +} + +function ArcCannon_Stop( weapon, player = null ) +{ + weapon.Signal( ARC_CANNON_SIGNAL_DEACTIVATED ) + StopChargeEffects( weapon, player ) + + weapon.StopWeaponEffect( "wpn_arc_cannon_electricity_fp", "wpn_arc_cannon_electricity" ) + weapon.StopWeaponSound( "arc_cannon_charged_loop" ) + weapon.StopWeaponSound( "arc_cannon_charge" ) +} + +function ArcCannon_ChargeBegin( weapon ) +{ + local weaponOwner = weapon.GetWeaponOwner() + local weaponScriptScope = weapon.GetScriptScope() + local useNormalChargeSounds = true + if( weapon.HasMod( "overcharge" ) ) + { + if ( weaponOwner.IsTitan() ) + { + local soul = weaponOwner.GetTitanSoul() + if ( soul.GetShieldHealth() > 0 ) + { + weapon.EmitWeaponSound( "arc_cannon_fastcharge" ) + useNormalChargeSounds = false + } + if ( IsServer() ) + thread ConvertTitanShieldIntoBonusCharge( soul, weapon ) + } + } + + if ( useNormalChargeSounds ) + { + weapon.EmitWeaponSound( "arc_cannon_charge" ) + } + + if( !("maxChargeTime" in weapon.s) ) + weapon.s.maxChargeTime <- weapon.GetWeaponModSetting( "charge_time" ) + + weapon.PlayWeaponEffectNoCull( "wpn_arc_cannon_charge_fp", "wpn_arc_cannon_charge", "muzzle_flash" ) + local chargeTime = weapon.GetWeaponChargeTime() + + if ( IsClient() ) + { + if ( !weapon.ShouldPredictProjectiles() ) + return + + if ( weaponOwner.IsPlayer() ) + weaponOwner.StartArcCannon(); + + local handle = weapon.AllocateHandleForViewmodelEffect( "wpn_arc_cannon_charge_fp" ) + if ( handle ) + EffectSkipForwardToTime( handle, chargeTime ) + + thread cl_ChargeRumble( weapon, ARC_CANNON_RUMBLE_TYPE_INDEX, ARC_CANNON_RUMBLE_CHARGE_MIN, ARC_CANNON_RUMBLE_CHARGE_MAX, ARC_CANNON_SIGNAL_CHARGEEND ) + } + thread ChargeEffects( weapon ) +} + +function ArcCannon_ChargeEnd( weapon, player = null ) +{ + if ( IsClient() && weapon.GetWeaponOwner() == GetLocalViewPlayer() ) + { + local weaponOwner + if ( player != null ) + weaponOwner = player + else + weaponOwner = weapon.GetWeaponOwner() + + if ( IsValid( weaponOwner ) && weaponOwner.IsPlayer() ) + weaponOwner.StopArcCannon() + } + if( IsValid( weapon ) ) + StopChargeEffects( weapon ) +} + +function StopChargeEffects( weapon, player = null ) +{ + weapon.Signal( ARC_CANNON_SIGNAL_CHARGEEND ) + + local weaponScriptScope = weapon.GetScriptScope() + weapon.StopWeaponSound( "arc_cannon_charge" ) + weapon.StopWeaponEffect( "wpn_arc_cannon_charge_fp", "wpn_arc_cannon_charge" ) + weapon.StopWeaponSound( "arc_cannon_fastcharge" ) + //weapon.StopWeaponEffect( "wpn_arc_cannon_charge_fp", "wpn_arc_cannon_charge" ) + weapon.StopWeaponEffect( "wpn_ARC_knob_FP", "wpn_ARC_knob" ) +} + +function ChargeEffects( weapon ) +{ + weapon.EndSignal( ARC_CANNON_SIGNAL_CHARGEEND ) + weapon.EndSignal( "OnDestroy" ) + + local player = weapon.GetWeaponOwner() + + wait ( weapon.s.maxChargeTime * GetArcCannonChargeFraction( weapon ) ) + + weapon.PlayWeaponEffectNoCull( "wpn_ARC_knob_FP", "wpn_ARC_knob", "SPINNING_KNOB" ) +} + +function ConvertTitanShieldIntoBonusCharge( soul, 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 + local shieldHealthMax = soul.GetShieldHealthMax() + local chargeRatio = GetArcCannonChargeFraction( weapon ) + + while( 1 ) + { + if( !IsValid( soul ) || !IsValid( weapon ) ) + break + + local baseCharge = weapon.GetWeaponChargeFraction() // + GetOverchargeBonusChargeFraction() + local charge = clamp ( baseCharge * ( 1 / chargeRatio ), 0.0, 1.0 ) + if( charge < 1.0 || maxShieldDecay > 0) + { + local 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.s.nextRegenTime = Time() + TITAN_SHIELD_REGEN_DELAY + + if( shieldDecayAmount > shieldHealth ) + bonusChargeFraction = bonusChargeFraction * ( shieldHealth / shieldDecayAmount ) + weapon.SetWeaponChargeFraction( baseCharge + bonusChargeFraction ) + } + wait 0.1 + } +} + +function FireArcCannon( weapon, attackParams ) +{ + local weaponScriptScope = weapon.GetScriptScope() + local owner = weapon.GetWeaponOwner() + + local baseCharge = weapon.GetWeaponChargeFraction() // + GetOverchargeBonusChargeFraction() + local charge = clamp ( baseCharge * ( 1 / GetArcCannonChargeFraction( weapon ) ), 0.0, 1.0 ) + local newVolume = GraphCapped( charge, 0.25, 1.0, 0.0, 1.0 ) + if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) ) + { + weapon.EmitWeaponSound( "arc_cannon_fire_SmallShot_Amped" ) + weaponScriptScope.PlayWeaponSoundWithVolume( "arc_cannon_fire_BigShot_Amped", newVolume ) + } + else + { + weapon.EmitWeaponSound( "arc_cannon_fire_SmallShot" ) + weaponScriptScope.PlayWeaponSoundWithVolume( "arc_cannon_Fire_BigShot", newVolume ) + } + + weapon.StopWeaponSound( "arc_cannon_charged_loop" ) + + weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 ) + + weapon.PlayWeaponEffect( "wpn_muzzleflash_arc_cannon_fp", "wpn_muzzleflash_arc_cannon", "muzzle_flash" ) + + StopChargeEffects( weapon ) + + local attachmentName = "muzzle_flash" + local attachmentIndex = weapon.LookupAttachment( attachmentName ) + Assert( attachmentIndex >= 0 ) + local muzzleOrigin = weapon.GetAttachmentOrigin( attachmentIndex ) + + //printt( "-------- FIRING ARC CANNON --------" ) + + local firstTargetInfo = GetFirstArcCannonTarget( weapon, attackParams ) + if ( !IsValid( firstTargetInfo.target ) ) + FireArcNoTargets( weapon, attackParams, muzzleOrigin ) + else + FireArcWithTargets( weapon, firstTargetInfo, attackParams, muzzleOrigin ) + + return 1 +} + +function GetFirstArcCannonTarget( weapon, attackParams ) +{ + local owner = weapon.GetWeaponOwner() + local coneHeight = weapon.GetMaxDamageFarDist() + + local angleToAxis = 8 // set this too high and auto-titans using it will error on GetVisibleEntitiesInCone + local ignoredEntities = [ owner, weapon ] + local traceMask = TRACE_MASK_SHOT + local flags = VIS_CONE_ENTS_TEST_HITBOXES // | VIS_CONE_ENTS_IGNORE_VORTEX + local antilagPlayer = null + if ( owner.IsPlayer() ) + { + angleToAxis = owner.GetAttackSpreadAngle() * 0.095 + antilagPlayer = owner + } + + local results + local 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 + local firstTargetInfo = {} + firstTargetInfo.target <- null + firstTargetInfo.hitLocation <- null + for ( local i = 0 ; i < 2 ; i++ ) + { + local missileCheck = i == 0 + local coneAngle = angleToAxis + if ( missileCheck ) + coneAngle *= 3.0 + + results = GetVisibleEntitiesInCone( attackParams.pos, attackParams.dir, coneHeight, coneAngle, ignoredEntities, traceMask, flags, antilagPlayer, false, false ) + foreach( result in results ) + { + local visibleEnt = result.entity + + if ( !IsValid( visibleEnt ) ) + continue + + local classname = IsServer() ? visibleEnt.GetClassname() : visibleEnt.GetSignifierName() + + if ( !( classname in level.arcCannonTargetClassnames ) ) + continue + + if ( "GetTeam" in visibleEnt ) + { + local visibleEntTeam = visibleEnt.GetTeam() + if ( visibleEntTeam == ownerTeam ) + continue + if ( IsEntANeutralMegaTurret( visibleEnt, ownerTeam ) ) + continue + } + + if ( missileCheck && classname != "rpg_missile" ) + continue + + if ( !missileCheck && classname == "rpg_missile" ) + 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 ) + + return firstTargetInfo +} + +function FireArcNoTargets( weapon, attackParams, muzzleOrigin ) +{ + Assert( IsValid( weapon ) ) + local player = weapon.GetWeaponOwner() + local chargeFrac = weapon.GetWeaponChargeFraction() + local beamVec = attackParams.dir * weapon.GetMaxDamageFarDist() + local playerEyePos = player.EyePosition() + local traceResults = TraceLineHighDetail( playerEyePos, (playerEyePos + beamVec), weapon, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE ) + local beamEnd = traceResults.endPos + + local vortexHit = VortexBulletHitCheck( player, playerEyePos, beamEnd ) + if ( vortexHit ) + { + if( IsServer() ) + { + local vortexWeapon = vortexHit.vortex.GetOwnerWeapon() + if( vortexWeapon && vortexWeapon.GetClassname() == "mp_titanweapon_vortex_shield" ) + VortexDrainedByImpact( vortexWeapon, weapon, null, null ) + } + beamEnd = vortexHit.hitPos + } + + local 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( IsServer() ) + CreateExplosion( beamEnd, 0, 0, 1, 1, player, 0, null, -1, false, ARC_CANNON_FX_TABLE ) +} + +function FireArcWithTargets( weapon, firstTargetInfo, attackParams, muzzleOrigin ) +{ + local beamStart = muzzleOrigin + local beamEnd + local player = weapon.GetWeaponOwner() + local chargeFrac = weapon.GetWeaponChargeFraction() + local 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 ) + 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 ) + + local 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 + local chainNum = 1 + thread ZapTargetRecursive( firstTargetInfo.target, zapInfo, zapInfo.muzzleOrigin, firstTargetInfo.hitLocation, chainNum ) +} + +function ZapTargetRecursive( target, zapInfo, beamStartPos, firstTargetBeamEndPos = null, chainNum = 1 ) +{ + if ( !IsValid( target ) ) + return + + if ( !IsValid( zapInfo.weapon ) ) + return + + Assert( target in zapInfo.zappedTargets ) + if ( chainNum > zapInfo.maxChains ) + return + local beamEndPos + if ( firstTargetBeamEndPos == null ) + beamEndPos = target.GetWorldSpaceCenter() + else + beamEndPos = firstTargetBeamEndPos + + waitthread ZapTarget( zapInfo, target, beamStartPos, beamEndPos, chainNum ) + + // Get other nearby targets we can chain to + if ( IsServer() ) + { + if ( !IsValid( target ) ) + return + + if ( !IsValid( zapInfo.weapon ) ) + return + + local chainTargets = GetArcCannonChainTargets( beamEndPos, target, zapInfo ) + foreach( 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 ( PlayerProgressionAllowed( zapInfo.player ) ) + zapInfo.player.SetPersistentVar( "ach_multikillArcRifle", true ) + if ( chainNum == 5 ) + UpdatePlayerStat( zapInfo.player, "misc_stats", "arcCannonMultiKills", 1 ) + } + } +} + +function ZapTarget( zapInfo, target, beamStartPos, beamEndPos, chainNum = 1 ) +{ + //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( firstBeam && IsServer() ) + CreateExplosion( beamEndPos, 0, 0, 1, 1, zapInfo.player, 0, null, -1, false, ARC_CANNON_FX_TABLE ) + thread CreateArcCannonBeam( zapInfo.weapon, target, beamStartPos, beamEndPos, zapInfo.player, ARC_CANNON_BEAM_LIFETIME, zapInfo.radius, boltWidth, 5, true, firstBeam ) + + if ( IsClient() ) + return + + local isMissile = ( target.GetClassname() == "rpg_missile" ) + if( !isMissile ) + wait ARC_CANNON_FORK_DELAY + else + wait 0.05 + + local deathPackage = damageTypes.ArcCannon + + local damageAmount + local damageMin + local damageMax + + if ( IsValid( target ) && IsValid( zapInfo.player ) ) + { + if ( target.GetArmorType() == ARMOR_TYPE_HEAVY ) + { + if ( IsValid( zapInfo.weapon ) ) + { + damageMin = zapInfo.weapon.GetWeaponModSetting( "damage_far_value_titanarmor" ) + damageMax = zapInfo.weapon.GetWeaponModSetting( "damage_near_value_titanarmor" ) + } + else + { + damageMin = GetWeaponInfoFileKeyField_Global( "mp_titanweapon_arc_cannon", "damage_far_value_titanarmor" ) + damageMax = GetWeaponInfoFileKeyField_Global( "mp_titanweapon_arc_cannon", "damage_near_value_titanarmor" ) + } + + // Due to auto-titans not charging, they do very little damage with this weapon against one another. + if ( zapInfo.player.IsNPC() ) + { + damageMin *= 7.0 + damageMax *= 7.0 + } + + // HACK; temp fix for non titan heavy armor targets (e.g. mega turret) + } + else + { + if ( IsValid( zapInfo.weapon ) ) + { + damageMin = zapInfo.weapon.GetWeaponModSetting( "damage_far_value" ) + damageMax = zapInfo.weapon.GetWeaponModSetting( "damage_near_value" ) + } + else + { + damageMin = GetWeaponInfoFileKeyField_Global( "mp_titanweapon_arc_cannon", "damage_far_value" ) + damageMax = GetWeaponInfoFileKeyField_Global( "mp_titanweapon_arc_cannon", "damage_near_value" ) + } + + if ( target.IsNPC() ) + { + damageMin *= 3.0 // more powerful against NPC humans so they die easy + damageMax *= 3.0 + } + } + + + // Scale damage amount based on how many chains deep we are + local chargeRatio = GetArcCannonChargeFraction( zapInfo.weapon ) + 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 = eDamageSourceId.mp_titanweapon_arc_cannon + + // 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 ) + { + local empDuration = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, ARC_CANNON_EMP_DURATION_MIN, ARC_CANNON_EMP_DURATION_MAX ) + + if ( target.IsPlayer() ) + { + local 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 ) + + local scale = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, ARC_CANNON_SLOW_SCALE_MIN, ARC_CANNON_SLOW_SCALE_MAX ) + thread EMP_SlowPlayer( target, scale, empDuration ) + } + else if ( zapInfo.chargeFrac >= ARC_CANNON_SCREEN_THRESHOLD ) + { + Remote.CallFunction_Replay( target, "ServerCallback_PilotEMP", empViewStrength, empDuration, ARC_CANNON_EMP_FADEOUT_DURATION ) + EmitSoundOnEntityOnlyToPlayer( target, target, ARC_CANNON_PILOT_SCREEN_SFX ) + } + } + + // Do 3rd person effect on the body + local effect = null + local tag = null + target.TakeDamage( damageAmount, zapInfo.player, zapInfo.player, { origin = zapInfo.player.GetOrigin(), force = Vector(0,0,0), scriptType = deathPackage, weapon = zapInfo.weapon, damageSourceId = dmgSourceID } ) + + 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() && effect != null && tag != null ) + { + 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.Explode() + } + } +} + + +function FadeOutSoundOnEntityAfterDelay( entity, soundAlias, delay, fadeTime ) +{ + + if ( !IsValid( entity ) ) + return + + entity.EndSignal( "OnDestroy" ) + wait delay + FadeOutSoundOnEntity( entity, soundAlias, fadeTime ) +} + + +function GetArcCannonChainTargets( fromOrigin, fromTarget, zapInfo ) +{ + Assert( IsServer() ) + + local results = [] + if ( !IsValid( zapInfo.player ) ) + return results + + local playerTeam = zapInfo.player.GetTeam() + local allTargets = GetArcCannonTargetsInRange( fromOrigin, playerTeam, zapInfo.weapon ) + allTargets = ArrayClosest( allTargets, fromOrigin ) + + local viewVector + if ( zapInfo.player.IsPlayer() ) + viewVector = zapInfo.player.GetViewVector() + else + viewVector = zapInfo.player.EyeAngles().AnglesToForward() + + 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.IsPlayer() ) + { + if ( ent.GetPlayerClass() == "operator" ) + continue + + if ( ent.GetPlayerClass() == "dronecontroller" ) + continue + + // 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 + } + + 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 = vecToEnt.Dot( 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 ) + } + + local 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 && 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 ( !IsValueInArray( results, ent ) ) + results.append( ent ) + } + + //printt( "NEARBY TARGETS VALID AND VISIBLE:", results.len() ) + + return results +} +Globalize( GetArcCannonChainTargets ) + + +function IsEntANeutralMegaTurret( ent, playerTeam ) +{ + if ( ent.GetClassname() != "npc_turret_mega" ) + return false + local entTeam = ent.GetTeam() + if ( entTeam == playerTeam ) + return false + if ( entTeam != GetOtherTeam( playerTeam ) ) + return true + + return false +} +Globalize( IsEntANeutralMegaTurret ) + +function ArcCannon_HideIdleEffect( weapon, delay ) +{ + //printt( "HideIdleEffect" ) + weapon.EndSignal( ARC_CANNON_SIGNAL_DEACTIVATED ) + weapon.StopWeaponEffect( "wpn_arc_cannon_electricity_fp", "wpn_arc_cannon_electricity" ) + wait delay + + if( !IsValid( weapon ) ) + return + + local 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.GetWeaponOwner().IsTitan() || weapon != weaponOwner.GetActiveWeapon() ) + return + + weapon.PlayWeaponEffectNoCull( "wpn_arc_cannon_electricity_fp", "wpn_arc_cannon_electricity", "muzzle_flash" ) + weapon.EmitWeaponSound( "arc_cannon_charged_loop" ) +} + +function AddToArcCannonTargets( ent ) +{ + AddToScriptManagedEntArray( level._arcCannonTargetsArrayID, ent ); +} + +function GetArcCannonTargets( origin, team ) +{ + local targets = GetScriptManagedEntArrayWithinCenter( level._arcCannonTargetsArrayID, team, origin, ARC_CANNON_TITAN_RANGE_CHAIN ) + + if ( ARC_CANNON_TARGETS_MISSILES ) + { + local enemyTeam = GetEnemyTeam( team ) + targets.extend( GetProjectileArrayEx( "rpg_missile", enemyTeam, origin, ARC_CANNON_TITAN_RANGE_CHAIN ) ) + } + + return targets +} +Globalize( GetArcCannonTargets ) + +function GetArcCannonTargetsInRange( origin, team, weapon ) +{ + local allTargets = GetArcCannonTargets( origin, team ) + local targetsInRange = [] + + local titanDistSq + local 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 ) + { + local d = DistanceSqr( target.GetOrigin(), origin ) + local validDist = target.IsTitan() ? titanDistSq : distSq + if ( d <= validDist ) + targetsInRange.append( target ) + } + + return targetsInRange +} + +function SortArcCannonTargets( weapon, targets ) +{ + Assert( targets.len() > 0 ) + local originalTargetCount = targets.len() + //printt( " sorting", originalTargetCount, "targets" ) + + local sortedTargets = [] + local lastEnt = weapon.GetWeaponOwner() + local closestIndex = null + local closestEnt = null + + while( targets.len() > 0 ) + { + closestEnt = null + closestIndex = null + + closestIndex = GetClosestIndex( targets, lastEnt.GetOrigin() ) + Assert( closestIndex != null ) + closestEnt = targets[ closestIndex ] + Assert( closestEnt != null ) + + sortedTargets.append( closestEnt ) + targets.remove( closestIndex ) + + lastEnt = closestEnt + } + + Assert( sortedTargets.len() == originalTargetCount ) + return sortedTargets +} + +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 ( IsClient() && firstBeam ) + thread CreateClientArcBeam( weapon, endPos, lifeDuration, target ) + + if ( IsClient() ) + return + + // Control point sets the end position of the effect + local cpEnd = CreateEntity( "info_placement_helper" ) + cpEnd.SetName( UniqueString( "arc_cannon_beam_cpEnd" ) ) + cpEnd.SetOrigin( endPos ) + DispatchSpawn( cpEnd, false ) + + local zapBeam = CreateEntity( "info_particle_system" ) + zapBeam.kv.cpoint1 = cpEnd.GetName() + + zapBeam.kv.effect_name = GetBeamEffect( weapon ) + + zapBeam.kv.start_active = 0 + zapBeam.SetOwner( player ) + zapBeam.SetOrigin( startPos ) + if ( firstBeam ) + { + zapBeam.kv.VisibilityFlags = 6 // everyone but owner + zapBeam.SetParent( player.GetActiveWeapon(), "muzzle_flash", false, 0.0 ) + } + DispatchSpawn( zapBeam ) + + zapBeam.Fire( "Start" ) + zapBeam.Fire( "StopPlayEndCap", "", lifeDuration ) + zapBeam.Kill( lifeDuration ) + cpEnd.Kill( lifeDuration ) +} + +function GetBeamEffect( weapon ) +{ + if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) ) + return ARC_CANNON_BEAM_EFFECT_MOD + + return ARC_CANNON_BEAM_EFFECT +} + +function CreateClientArcBeam( weapon, endPos, lifeDuration, target ) +{ + Assert( IsClient() ) + + local beamEffect = GetBeamEffect( weapon ) + + weapon.PlayWeaponEffect( beamEffect, null, "muzzle_flash" ) + local handle = weapon.AllocateHandleForViewmodelEffect( beamEffect ) + 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, null ) +} + +function ClientDestroyCallback_ArcCannon_Stop( entity ) +{ + ArcCannon_Stop( entity ) +} + +function GetArcCannonChargeFraction( weapon ) +{ + if ( IsValid( weapon ) ) + { + local chargeRatio = ARC_CANNON_DAMAGE_CHARGE_RATIO + if( weapon.HasModDefined( "capacitor" ) && weapon.HasMod( "capacitor" ) ) + chargeRatio = ARC_CANNON_CAPACITOR_CHARGE_RATIO + if( weapon.GetWeaponModSetting( "is_burn_mod" ) ) + chargeRatio = ARC_CANNON_DAMAGE_CHARGE_RATIO_BURN + return chargeRatio + } + + return 0 +}
\ No newline at end of file |