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