global function Smokescreen_Init global function Smokescreen global function IsOriginTouchingSmokescreen global function IsRayTouchingSmokescreen #if DEV const bool SMOKESCREEN_DEBUG = false #endif global struct SmokescreenStruct { vector origin vector angles bool fxUseWeaponOrProjectileAngles = false float lifetime = 5.0 int ownerTeam = TEAM_ANY asset smokescreenFX = FX_ELECTRIC_SMOKESCREEN float fxXYRadius = 230.0 // single fx xy radius used to create nospawn area and block traces float fxZRadius = 170.0 // single fx z radius used to create nospawn area and block traces string deploySound1p = SFX_SMOKE_DEPLOY_1P string deploySound3p = SFX_SMOKE_DEPLOY_3P string stopSound1p = "" string stopSound3p = "" int damageSource = eDamageSourceId.mp_titanability_smoke bool blockLOS = true bool shouldHibernate = true bool isElectric = true entity attacker entity inflictor entity weaponOrProjectile float damageDelay = 2.0 float damageInnerRadius = 320.0 float damageOuterRadius = 350.0 float dangerousAreaRadius = -1.0 int dpsPilot = 30 int dpsTitan = 2200 array<vector> fxOffsets } struct SmokescreenFXStruct { vector center // center of all fx positions vector mins // approx mins of all fx relative to center vector maxs // approx maxs of all fx relative to center float radius // approx radius of all fx relative to center array<vector> fxWorldPositions int ownerTeam = TEAM_ANY } struct { array<SmokescreenFXStruct> allSmokescreenFX table<entity, float> nextSmokeSoundTime } file void function Smokescreen_Init() { PrecacheParticleSystem( FX_ELECTRIC_SMOKESCREEN ) PrecacheParticleSystem( FX_ELECTRIC_SMOKESCREEN_BURN ) #if MP PrecacheParticleSystem( FX_ELECTRIC_SMOKESCREEN_HEAL ) #endif PrecacheParticleSystem( FX_GRENADE_SMOKESCREEN ) PrecacheSprite( $"sprites/physbeam.vmt" ) PrecacheSprite( $"sprites/glow01.vmt" ) #if SERVER AddDamageCallbackSourceID( eDamageSourceId.mp_titanability_smoke, TitanElectricSmoke_DamagedPlayerOrNPC ) AddDamageCallbackSourceID( eDamageSourceId.mp_weapon_grenade_electric_smoke, GrenadeElectricSmoke_DamagedPlayerOrNPC ) #endif } void function Smokescreen( SmokescreenStruct smokescreen ) { SmokescreenFXStruct fxInfo = Smokescreen_CalculateFXStruct( smokescreen ) file.allSmokescreenFX.append( fxInfo ) array<entity> thermiteBurns = GetActiveThermiteBurnsWithinRadius( fxInfo.center, fxInfo.radius ) foreach ( thermiteBurn in thermiteBurns ) { entity owner = thermiteBurn.GetOwner() if ( IsValid( owner ) && owner.GetTeam() != smokescreen.ownerTeam ) thermiteBurn.Destroy() } entity traceBlocker if ( smokescreen.blockLOS ) traceBlocker = Smokescreen_CreateTraceBlockerVol( smokescreen, fxInfo ) #if DEV if ( SMOKESCREEN_DEBUG ) DebugDrawCircle( fxInfo.center, <0,0,0>, fxInfo.radius + 240.0, 255, 255, 0, true, smokescreen.lifetime ) #endif CreateNoSpawnArea( TEAM_ANY, TEAM_ANY, fxInfo.center, smokescreen.lifetime, fxInfo.radius + 240.0 ) if ( IsValid( smokescreen.attacker ) && smokescreen.attacker.IsPlayer() ) { EmitSoundAtPositionExceptToPlayer( TEAM_ANY, fxInfo.center, smokescreen.attacker, smokescreen.deploySound3p ) EmitSoundAtPositionOnlyToPlayer( TEAM_ANY, fxInfo.center, smokescreen.attacker, smokescreen.deploySound1p) } else { EmitSoundAtPosition( TEAM_ANY, fxInfo.center, smokescreen.deploySound3p ) } array<entity> fxEntities = SmokescreenFX( smokescreen, fxInfo ) if ( smokescreen.isElectric ) thread SmokescreenAffectsEntitiesInArea( smokescreen, fxInfo ) //thread CreateSmokeSightTrigger( fxInfo.center, smokescreen.ownerTeam, smokescreen.lifetime ) // disabling for now, this should use the calculated radius if reenabled thread DestroySmokescreen( smokescreen, smokescreen.lifetime, fxInfo, traceBlocker, fxEntities ) } SmokescreenFXStruct function Smokescreen_CalculateFXStruct( SmokescreenStruct smokescreen ) { SmokescreenFXStruct fxInfo foreach ( i, position in smokescreen.fxOffsets ) { //mins if ( i == 0 || position.x < fxInfo.mins.x ) fxInfo.mins = <position.x, fxInfo.mins.y, fxInfo.mins.z> if ( i == 0 || position.y < fxInfo.mins.y ) fxInfo.mins = <fxInfo.mins.x, position.y, fxInfo.mins.z> if ( i == 0 || position.z < fxInfo.mins.z ) fxInfo.mins = <fxInfo.mins.x, fxInfo.mins.y, position.z> // maxs if ( i == 0 || position.x > fxInfo.maxs.x ) fxInfo.maxs = <position.x, fxInfo.maxs.y, fxInfo.maxs.z> if ( i == 0 || position.y > fxInfo.maxs.y ) fxInfo.maxs = <fxInfo.maxs.x, position.y, fxInfo.maxs.z> if ( i == 0 || position.z > fxInfo.maxs.z ) fxInfo.maxs = <fxInfo.maxs.x, fxInfo.maxs.y, position.z> } vector offsetCenter = fxInfo.mins + ( fxInfo.maxs - fxInfo.mins ) * 0.5 float xyRadius = smokescreen.fxXYRadius * 0.7071 float zRadius = smokescreen.fxZRadius * 0.7071 fxInfo.mins = <fxInfo.mins.x - xyRadius, fxInfo.mins.y - xyRadius, fxInfo.mins.z - zRadius> - offsetCenter fxInfo.maxs = <fxInfo.maxs.x + xyRadius, fxInfo.maxs.y + xyRadius, fxInfo.maxs.z + zRadius> - offsetCenter float radiusSqr float singleFXRadius = max( smokescreen.fxXYRadius, smokescreen.fxZRadius ) vector forward = AnglesToForward( smokescreen.angles ) vector right = AnglesToRight( smokescreen.angles ) vector up = AnglesToUp( smokescreen.angles ) foreach ( i, position in smokescreen.fxOffsets ) { float distanceSqr = DistanceSqr( position, offsetCenter ) if ( radiusSqr < distanceSqr ) radiusSqr = distanceSqr fxInfo.fxWorldPositions.append( smokescreen.origin + ( position.x * forward ) + ( position.y * right ) + ( position.z * up ) ) } fxInfo.center = smokescreen.origin + ( offsetCenter.x * forward ) + ( offsetCenter.y * right ) + ( offsetCenter.z * up ) fxInfo.radius = sqrt( radiusSqr ) + singleFXRadius fxInfo.ownerTeam = smokescreen.ownerTeam return fxInfo } void function SmokescreenAffectsEntitiesInArea( SmokescreenStruct smokescreen, SmokescreenFXStruct fxInfo ) { float startTime = Time() float tickRate = 0.1 float dpsPilot = smokescreen.dpsPilot * tickRate float dpsTitan = smokescreen.dpsTitan * tickRate Assert( dpsPilot || dpsTitan > 0, "Electric smokescreen with 0 damage created" ) entity aiDangerTarget = CreateEntity( "info_target" ) DispatchSpawn( aiDangerTarget ) aiDangerTarget.SetOrigin( fxInfo.center ) SetTeam( aiDangerTarget, smokescreen.ownerTeam ) float dangerousAreaRadius = smokescreen.damageOuterRadius if ( smokescreen.dangerousAreaRadius != -1.0 ) dangerousAreaRadius = smokescreen.dangerousAreaRadius AI_CreateDangerousArea_Static( aiDangerTarget, smokescreen.weaponOrProjectile, dangerousAreaRadius, TEAM_INVALID, true, true, fxInfo.center ) OnThreadEnd( function () : ( aiDangerTarget ) { aiDangerTarget.Destroy() } ) wait smokescreen.damageDelay while ( Time() - startTime <= smokescreen.lifetime ) { #if DEV if ( SMOKESCREEN_DEBUG ) { DebugDrawCircle( fxInfo.center, <0,0,0>, smokescreen.damageInnerRadius, 255, 0, 0, true, tickRate ) DebugDrawCircle( fxInfo.center, <0,0,0>, smokescreen.damageOuterRadius, 255, 0, 0, true, tickRate ) } #endif RadiusDamage( fxInfo.center, // center smokescreen.attacker, // attacker smokescreen.inflictor, // inflictor dpsPilot, // damage dpsTitan, // damageHeavyArmor smokescreen.damageInnerRadius, // innerRadius smokescreen.damageOuterRadius, // outerRadius SF_ENVEXPLOSION_MASK_BRUSHONLY, // flags 0.0, // distanceFromAttacker 0.0, // explosionForce DF_ELECTRICAL | DF_NO_HITBEEP, // scriptDamageFlags smokescreen.damageSource ) // scriptDamageSourceIdentifier wait tickRate } } entity function Smokescreen_CreateTraceBlockerVol( SmokescreenStruct smokescreen, SmokescreenFXStruct fxInfo ) { entity traceBlockerVol = CreateEntity( "trace_volume" ) traceBlockerVol.kv.targetname = UniqueString( "smokescreen_traceblocker_vol" ) traceBlockerVol.kv.origin = fxInfo.center traceBlockerVol.kv.angles = smokescreen.angles DispatchSpawn( traceBlockerVol ) traceBlockerVol.SetBox( fxInfo.mins * 0.9, fxInfo.maxs * 0.9 ) #if DEV if ( SMOKESCREEN_DEBUG ) DrawAngledBox( fxInfo.center, smokescreen.angles, fxInfo.mins, fxInfo.maxs, 255, 0, 0, true, smokescreen.lifetime - 0.6 ) #endif return traceBlockerVol } array<entity> function SmokescreenFX( SmokescreenStruct smokescreen, SmokescreenFXStruct fxInfo ) { array<entity> fxEntities foreach ( position in fxInfo.fxWorldPositions ) { #if DEV if ( SMOKESCREEN_DEBUG ) DebugDrawCircle( position, <0.0, 0.0, 0.0>, smokescreen.fxXYRadius, 0, 0, 255, true, smokescreen.lifetime ) #endif int fxID = GetParticleSystemIndex( smokescreen.smokescreenFX ) vector angles = smokescreen.fxUseWeaponOrProjectileAngles ? smokescreen.weaponOrProjectile.GetAngles() : <0.0, 0.0, 0.0> entity fxEnt = StartParticleEffectInWorld_ReturnEntity( fxID, position, angles ) float fxLife = smokescreen.lifetime EffectSetControlPointVector( fxEnt, 1, <fxLife, 0.0, 0.0> ) if ( !smokescreen.shouldHibernate ) fxEnt.DisableHibernation() fxEntities.append( fxEnt ) } return fxEntities } void function DestroySmokescreen( SmokescreenStruct smokescreen, float lifetime, SmokescreenFXStruct fxInfo, entity traceBlocker, array<entity> fxEntities ) { float timeToWait = 0.0 timeToWait = max( lifetime - 0.5, 0.0 ) wait( timeToWait ) if ( IsValid( traceBlocker ) ) traceBlocker.Destroy() file.allSmokescreenFX.fastremovebyvalue( fxInfo ) StopSoundAtPosition( fxInfo.center, smokescreen.deploySound1p ) StopSoundAtPosition( fxInfo.center, smokescreen.deploySound3p ) if ( IsValid( smokescreen.attacker ) && smokescreen.attacker.IsPlayer() ) { if ( smokescreen.stopSound3p != "" ) EmitSoundAtPositionExceptToPlayer( TEAM_ANY, fxInfo.center, smokescreen.attacker, smokescreen.stopSound3p ) if ( smokescreen.stopSound1p != "" ) EmitSoundAtPositionOnlyToPlayer( TEAM_ANY, fxInfo.center, smokescreen.attacker, smokescreen.stopSound1p) } else { if ( smokescreen.stopSound3p != "" ) EmitSoundAtPosition( TEAM_ANY, fxInfo.center, smokescreen.stopSound3p ) } timeToWait = max( ( lifetime + 0.1 ) - timeToWait, 0.0 ) wait( timeToWait ) foreach ( fxEnt in fxEntities ) { if ( IsValid( fxEnt ) ) fxEnt.Destroy() } } bool function IsOriginTouchingSmokescreen( vector origin, int teamToIgnore = TEAM_UNASSIGNED ) { foreach ( fxInfo in file.allSmokescreenFX ) { if ( teamToIgnore == fxInfo.ownerTeam ) continue if ( DistanceSqr( origin, fxInfo.center ) < fxInfo.radius * fxInfo.radius ) return true } return false } bool function IsRayTouchingSmokescreen( vector rayStart, vector rayEnd, int teamToIgnore = TEAM_UNASSIGNED ) { foreach ( fxInfo in file.allSmokescreenFX ) { if ( teamToIgnore == fxInfo.ownerTeam ) continue if ( IntersectRayWithSphere( rayStart, rayEnd, fxInfo.center, fxInfo.radius ).result ) return true } return false } #if SERVER void function TitanElectricSmoke_DamagedPlayerOrNPC( entity ent, var damageInfo ) { if ( !IsAlive( ent ) ) return entity attacker = DamageInfo_GetAttacker( damageInfo ) if ( ent.GetTeam() == attacker.GetTeam() ) { DamageInfo_SetDamage( damageInfo, 0 ) return } PlayDamageSounds( ent, attacker, ELECTRIC_SMOKESCREEN_SFX_DAMAGE_TITAN_1P, ELECTRIC_SMOKESCREEN_SFX_DAMAGE_TITAN_3P, ELECTRIC_SMOKESCREEN_SFX_DAMAGE_PILOT_1P, ELECTRIC_SMOKESCREEN_SFX_DAMAGE_PILOT_3P ) } void function GrenadeElectricSmoke_DamagedPlayerOrNPC( entity ent, var damageInfo ) { if ( !IsAlive( ent ) ) return entity attacker = DamageInfo_GetAttacker( damageInfo ) PlayDamageSounds( ent, attacker, ELECTRIC_SMOKE_GRENADE_SFX_DAMAGE_TITAN_1P, ELECTRIC_SMOKE_GRENADE_SFX_DAMAGE_TITAN_3P, ELECTRIC_SMOKE_GRENADE_SFX_DAMAGE_PILOT_1P, ELECTRIC_SMOKE_GRENADE_SFX_DAMAGE_PILOT_3P ) } void function PlayDamageSounds( entity ent, entity attacker, string titan1P_SFX, string titan3P_SFX, string pilot1P_SFX, string pilot3P_SFX ) { float currentTime = Time() if ( !( ent in file.nextSmokeSoundTime ) ) { if ( ent.IsPlayer() ) file.nextSmokeSoundTime[ ent ] <- currentTime else file.nextSmokeSoundTime[ ent ] <- currentTime + RandomFloat( 0.5 ) } if ( file.nextSmokeSoundTime[ ent ] <= currentTime ) { if ( ent.IsPlayer() ) { if ( ent.IsTitan() ) { EmitSoundOnEntityExceptToPlayer( ent, ent, titan3P_SFX ) EmitSoundOnEntityOnlyToPlayer( ent, ent, titan1P_SFX ) file.nextSmokeSoundTime[ ent ] = currentTime + RandomFloatRange( 0.75, 1.25 ) } else { EmitSoundOnEntityExceptToPlayer( ent, ent, pilot3P_SFX ) EmitSoundOnEntityOnlyToPlayer( ent, ent, pilot1P_SFX ) } if ( IsValid( attacker ) && attacker.IsPlayer() ) EmitSoundOnEntityOnlyToPlayer( attacker, attacker, "Player.Hitbeep" ) } else { if ( ent.IsTitan() ) EmitSoundOnEntity( ent, titan3P_SFX ) else if ( IsHumanSized( ent ) ) EmitSoundOnEntity( ent, pilot3P_SFX ) } file.nextSmokeSoundTime[ ent ] = currentTime + RandomFloatRange( 0.75, 1.25 ) } } #endif