#include maps\mp\_utility; #include common_scripts\utility; // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // // Init // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // CONST_LOCK_ON_TIME_MSEC = 1500; LGM_init( fxSplit, fxHoming ) { level._effect[ "laser_guided_launcher_missile_split" ] = LoadFX( fxSplit ); level._effect[ "laser_guided_launcher_missile_spawn_homing" ] = LoadFX( fxHoming ); } // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // // Monitor Player Weapon for Use as CAC Launcher // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // // Function to use if you want to have a laser guided launcher as // a launcher in CAC. Just thread this watch off in _weapons.gsc::init() LGM_update_launcherUsage( weaponName, weaponNameHoming ) { self endon( "death" ); self endon( "disconnect" ); self endon( "faux_spawn" ); self thread LGM_monitorLaser(); weaponCurr = self GetCurrentWeapon(); while ( 1 ) { while ( weaponCurr != weaponName ) { self waittill( "weapon_change", weaponCurr ); } self childthread LGM_firing_monitorMissileFire( weaponCurr, weaponNameHoming ); self waittill( "weapon_change", weaponCurr ); self LGM_firing_endMissileFire(); } } LGM_monitorLaser() { self endon( "LGM_player_endMonitorFire" ); self waittill_any( "death", "disconnect" ); if ( IsDefined( self ) ) { self LGM_disableLaser(); } } // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // // Monitor Player Firing // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // LASER_GUIDED_MISSILE_LASER_TRACE_CLOSE_TIME_MSEC = 400; // Time after firing that the trace distance is short. This forces the missiles forward. LASER_GUIDED_MISSILE_LASER_TRACE_LENGTH_SHORT = 800; LASER_GUIDED_MISSILE_LASER_TRACE_LENGTH = 8000; LASER_GUIDED_MISSILE_DELAY_CHILDREN_SPAWN = 0.35; LASER_GUIDED_MISSILE_DELAY_CHILDREN_TRACK = 0.1; LASER_GUIDED_MISSILE_PITCH_CHILDREN_DIVERGE = 20; LASER_GUIDED_MISSILE_YAW_CHILDREN_DIVERGE = 20; LGM_firing_endMissileFire() { self LGM_disableLaser(); self notify( "LGM_player_endMonitorFire" ); } LGM_firing_monitorMissileFire( weaponName, weaponNameChild, weaponNameHoming ) { self endon( "LGM_player_endMonitorFire" ); self LGM_enableLaser(); entTarget = undefined; while ( 1 ) { missile = undefined; self waittill( "missile_fire", missile, weaponNotified ); // Ignore missiles fired by script if ( IsDefined( missile.isMagicBullet ) && missile.isMagicBullet ) continue; // The hind magic bullets missiles on behalf of the player, so ignore without assert. if ( weaponNotified != weaponName ) continue; if ( !IsDefined( entTarget ) ) { entTarget = LGM_requestMissileGuideEnt( self ); } self thread LGM_firing_delaySpawnChildren( weaponName, weaponNameChild, weaponNameHoming, LASER_GUIDED_MISSILE_DELAY_CHILDREN_SPAWN, LASER_GUIDED_MISSILE_DELAY_CHILDREN_TRACK, missile, entTarget ); } } LGM_firing_delaySpawnChildren( weaponName, weaponNameChild, weaponNameHoming, delaySpawn, delayTrack, missile, entTarget ) { // If the player fires rapidly, cancel the previous missile spawn self notify( "monitor_laserGuidedMissile_delaySpawnChildren" ); self endon( "monitor_laserGuidedMissile_delaySpawnChildren" ); // If the player dies this function needs to be cleared up immediately. This is because // the target ent gets scrubbed on death so any created missiles may have been // removed from the array self endon( "death" ); self endon( "LGM_player_endMonitorFire" ); // Only let one set of rockets be guided by releasing // any previously fired rockets LGM_missilesNotifyAndRelease( entTarget ); wait( delaySpawn ); // Exit if missile has blown up if ( !IsValidMissile( missile ) ) return; missileOrigin = missile.origin; missileFwd = AnglesToForward( missile.angles ); missileUp = AnglesToUp( missile.angles ); missileRight = AnglesToRight( missile.angles ); missile Delete(); // Spawn missile break apart fx PlayFX( level._effect[ "laser_guided_launcher_missile_split" ], missileOrigin, missileFwd, missileUp ); missiles = []; for ( i = 0; i < 2; i++ ) { pitch = LASER_GUIDED_MISSILE_PITCH_CHILDREN_DIVERGE; // Default Upwards Pitch: Forward and Up n degrees yaw = 0; if ( i == 0 ) // Right Missile: Up 45 degrees and then 45 degrees around Z { yaw = LASER_GUIDED_MISSILE_YAW_CHILDREN_DIVERGE; } else if ( i == 1 ) // Left Missile: Up 45 degrees and then -45 degrees around Z { yaw = -1 * LASER_GUIDED_MISSILE_YAW_CHILDREN_DIVERGE; } else if ( i == 2 )// Middle Missile: Already rotated up { // All done } childMissileFwd = RotatePointAroundVector( missileRight, missileFwd, pitch ); childMissileFwd = RotatePointAroundVector( missileUp, childMissileFwd, yaw ); missileChild = MagicBullet( weaponNameChild, missileOrigin, missileOrigin + childMissileFwd * 180, self ); missileChild.isMagicBullet = true; missiles[ missiles.size ] = missileChild; // Don't spawn missiles on the same frame waitframe(); } wait( delayTrack ); missiles = LGM_removeInvalidMissiles( missiles ); if ( missiles.size > 0 ) { foreach ( missChild in missiles ) { entTarget.missilesChasing[ entTarget.missilesChasing.size ] = missChild; missChild Missile_SetTargetEnt( entTarget ); self thread LGM_onMissileNotifies( entTarget, missChild ); } self thread LGM_firing_monitorPlayerAim( entTarget, weaponNameHoming ); } } LGM_onMissileNotifies( entTarget, missile ) { missile waittill_any( "death", "missile_pairedWithFlare", "LGM_missile_abandoned" ); if ( IsDefined( entTarget.missilesChasing ) && entTarget.missilesChasing.size > 0 ) { entTarget.missilesChasing = array_remove( entTarget.missilesChasing, missile ); entTarget.missilesChasing = LGM_removeInvalidMissiles( entTarget.missilesChasing ); } if ( !IsDefined( entTarget.missilesChasing ) || entTarget.missilesChasing.size == 0 ) { self notify( "LGM_player_allMissilesDestroyed" ); } } // Handles initial lock visual / audio fx LGM_firing_monitorPlayerAim( entTarget, weaponNameHoming ) { self notify( "LGM_player_newMissilesFired" ); self endon( "LGM_player_newMissilesFired" ); self endon( "LGM_player_allMissilesDestroyed" ); self endon( "LGM_player_endMonitorFire" ); self endon( "death" ); self endon( "disconnect" ); originGoal = undefined; targetVeh = undefined; lockOnTime = undefined; lockedOn = false; timeTraceFar = GetTime() + LASER_GUIDED_MISSILE_LASER_TRACE_CLOSE_TIME_MSEC; while ( IsDefined( entTarget.missilesChasing ) && entTarget.missilesChasing.size > 0 ) { targetLook = self LGM_targetFind(); if ( !IsDefined( targetLook ) ) { // If there was a previous target, clear that target and // notify systmes watching the missiles that the // target has changed if ( IsDefined( targetVeh ) ) { self notify( "LGM_player_targetLost" ); targetVeh = undefined; foreach ( missile in entTarget.missilesChasing ) { missile notify( "missile_targetChanged" ); } } lockOnTime = undefined; lockedOn = false; traceDist = ter_op( GetTime() > timeTraceFar, LASER_GUIDED_MISSILE_LASER_TRACE_LENGTH, LASER_GUIDED_MISSILE_LASER_TRACE_LENGTH_SHORT ); viewDir = AnglesToForward( self GetPlayerAngles() ); startPos = self GetEye() + viewDir * 12; trace = BulletTrace( startPos, startPos + viewDir * traceDist, true, self, false, false, false ); originGoal = trace[ "position" ]; } else { originGoal = targetLook.origin; newTarget = !IsDefined( targetVeh ) || targetLook != targetVeh; targetVeh = targetLook; if ( newTarget || !IsDefined( lockOnTime ) ) { lockOnTime = GetTime() + CONST_LOCK_ON_TIME_MSEC; level thread LGM_locking_think( targetVeh, self ); } else if ( GetTime() >= lockOnTime ) { // incoming notify was fired as soon as the player looked at // the target lockedOn = true; self notify( "LGM_player_lockedOn" ); } if ( lockedOn ) { // In case the current live missiles are paired with flares // this script update wait untill after they're removed // from missilesChasing waittillframeend; if ( entTarget.missilesChasing.size > 0 ) { missileOrigins = []; foreach ( missile in entTarget.missilesChasing ) { if ( !IsValidMissile( missile ) ) continue; missileOrigins[ missileOrigins.size ] = missile.origin; missile notify( "missile_targetChanged" ); missile notify( "LGM_missile_abandoned" ); missile Delete(); } if ( missileOrigins.size > 0 ) { level thread LGM_locked_think( targetVeh, self, weaponNameHoming, missileOrigins ); } entTarget.missilesChasing = []; } else { // All missiles were removed during the waittillframeend break; } } else if ( newTarget ) { LGM_targetNotifyMissiles( targetVeh, self, entTarget.missilesChasing ); } } entTarget.origin = originGoal; waitframe(); } } // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // // Monitor LaserGuided Missile Ent Pool // - Prevent too many ents being created // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // TARGET_ENT_COUNT_PREFERRED_MAX = 4; // The pool of target ents can get larger than this but should shrink back down when less are needed. LGM_requestMissileGuideEnt( player ) { if ( !IsDefined( level.laserGuidedMissileEnts_inUse ) ) { level.laserGuidedMissileEnts_inUse = []; } if ( !IsDefined( level.laserGuidedMissileEnts_ready ) ) { level.laserGuidedMissileEnts_ready = []; } ent = undefined; if ( level.laserGuidedMissileEnts_ready.size ) { ent = level.laserGuidedMissileEnts_ready[ 0 ]; level.laserGuidedMissileEnts_ready = array_remove( level.laserGuidedMissileEnts_ready, ent ); } else { ent = spawn( "script_origin", player.origin ); } level.laserGuidedMissileEnts_inUse[ level.laserGuidedMissileEnts_inUse.size ] = ent; level thread LGM_monitorLaserEntCleanUp( ent, player ); ent.missilesChasing = []; return ent; } LGM_monitorLaserEntCleanUp( entTarget, player ) { player waittill_any( "death", "disconnect", "LGM_player_endMonitorFire" ); AssertEx( array_contains( level.laserGuidedMissileEnts_inUse, entTarget ), "LGM_monitorLaserEntCleanUp() attempting to clean up laser target ent not currently in use." ); // Scrub ent clean AssertEx( IsDefined( entTarget.missilesChasing ) && IsArray( entTarget.missilesChasing ), "LGM_monitorLaserEntCleanUp() given missile ent with now missile array." ); foreach ( missile in entTarget.missilesChasing ) { if ( IsValidMissile( missile ) ) { missile Missile_ClearTarget(); } } entTarget.missilesChasing = undefined; // Move ent fron in use to ready array level.laserGuidedMissileEnts_inUse = array_remove( level.laserGuidedMissileEnts_inUse, entTarget ); // If the too many ents exist delete it, otherwise add it to ready array if ( level.laserGuidedMissileEnts_ready.size + level.laserGuidedMissileEnts_inUse.size < TARGET_ENT_COUNT_PREFERRED_MAX ) { level.laserGuidedMissileEnts_ready[ level.laserGuidedMissileEnts_ready.size ] = entTarget; } else { entTarget Delete(); } } // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // // Monitor Locking and Locked On // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // LGM_locking_think( targetVeh, player ) { AssertEx( IsDefined( player ), "LGM_locking_think called with undefined player." ); outline = outlineEnableForPlayer( targetVeh, "orange", player, true, "killstreak_personal" ); level thread LGM_locking_loopSound( player, "maaws_reticle_tracking", 1.5, "LGM_player_lockingDone" ); level thread LGM_locking_notifyOnTargetDeath( targetVeh, player ); player waittill_any( "death", // targetting player died "disconnect", // targetting player left game "LGM_player_endMonitorFire", // ks system stopped launcher logic "LGM_player_newMissilesFired", // player fired again, these missiles are going to be abandoned "LGM_player_targetLost", // player looked away or new enemy came into view during lock on "LGM_player_lockedOn", // player obtained full lock, this outlin is removed, a new outlin call is made "LGM_player_allMissilesDestroyed", // the current set of tracked missiles all died or were paired with flares "LGM_player_targetDied" // target destroyed ); // Some entities delete instantly on death and may // be removed at this point if ( IsDefined( targetVeh ) ) { outlineDisable( outline, targetVeh ); } if ( IsDefined( player ) ) { player notify( "LGM_player_lockingDone" ); player StopLocalSound( "maaws_reticle_tracking" ); } } LGM_locked_missileOnDeath( missile, targetVeh, groupID ) { targetVeh endon( "death" ); missile waittill( "death" ); targetVeh.LG_missilesLocked[ groupID ] = array_remove( targetVeh.LG_missilesLocked[ groupID ], missile ); if ( targetVeh.LG_missilesLocked[ groupID ].size == 0 ) { targetVeh.LG_missilesLocked[ groupID ] = undefined; targetVeh notify( "LGM_target_lockedMissilesDestroyed" ); } } LGM_locking_notifyOnTargetDeath( target, player ) { player endon( "death" ); player endon( "disconnect" ); player endon( "LGM_player_lockingDone" ); target waittill( "death" ); player notify( "LGM_player_targetDied" ); } LGM_locking_loopSound( player, sound, time, endonPlayer ) { player endon( "death" ); player endon( "disconnect" ); player endon( endOnPlayer ); while ( 1 ) { player PlayLocalSound( sound ); wait( time ); } } LGM_locked_spawnMissiles( target, player, weaponNameHoming, missileOrigins ) { target endon( "death" ); player endon( "death" ); player endon( "disconnect" ); missilesLocked = []; for ( i = 0; i < missileOrigins.size; i++ ) { missileChild = MagicBullet( weaponNameHoming, missileOrigins[ i ], target.origin, player ); missileChild.isMagicBullet = true; missilesLocked[ missilesLocked.size ] = missileChild; PlayFX( level._effect[ "laser_guided_launcher_missile_spawn_homing" ], missileChild.origin, AnglesToForward( missileChild.angles ), AnglesToUp( missileChild.angles ) ); // Don't spawn missiles on the same frame waitframe(); } return missilesLocked; } // Handles locked on visual / audio fx LGM_locked_think( targetVeh, player, weaponNameHoming, missileOrigins ) { AssertEx( missileOrigins.size > 0, "LGM_locked_think() passed empty missile origin array." ); if ( missileOrigins.size == 0 ) return; missilesLocked = LGM_locked_spawnMissiles( targetVeh, player, weaponNameHoming, missileOrigins ); // If undefined the player died or the target died if ( !IsDefined( missilesLocked ) ) return; // In case a missile died after the above wait frame missilesLocked = LGM_removeInvalidMissiles( missilesLocked ); if ( missilesLocked.size == 0 ) return; // Visual and audio fx player PlayLocalSound( "maaws_reticle_locked" ); outlineID = outlineEnableForPlayer( targetVeh, "red", player, false, "killstreak_personal" ); // Give missiles their target with an offset targetOffset = LGM_getTargetOffset( targetVeh ); foreach ( mChild in missilesLocked ) { mChild missile_setTargetAndFlightMode( targetVeh, "direct", targetOffset ); LGM_targetNotifyMissiles( targetVeh, player, missilesLocked ); } if ( !IsDefined( targetVeh.LG_missilesLocked ) ) { targetVeh.LG_missilesLocked = []; } // Because multiple sets of missiles from the same or different players // can be tracking the helicopter at the same timed, add the missiles // to an array by the unique outline ID targetVeh.LG_missilesLocked[ outlineID ] = missilesLocked; foreach ( vMiss in missilesLocked ) { level thread LGM_locked_missileOnDeath( vMiss, targetVeh, outlineID ); } outlineOn = true; while ( outlineOn ) { msg = targetVeh waittill_any_return( "death", "LGM_target_lockedMissilesDestroyed" ); if ( msg == "death" ) { outlineOn = false; if ( IsDefined( targetVeh ) ) { targetVeh.LG_missilesLocked[ outlineID ] = undefined; } } else if ( msg == "LGM_target_lockedMissilesDestroyed" ) { // Two sets of missiles could potentially throw the "LGM_target_lockedMissilesDestroyed" // notification on the same frame. Wait until frame end and then check to see if this // outline's missiles are all gone waittillframeend; if ( !IsDefined( targetVeh.LG_missilesLocked[ outlineID ] ) || targetVeh.LG_missilesLocked[ outlineID ].size == 0 ) { outlineOn = false; } } } // Targetveh may be deleted at this point if ( IsDefined( targetVeh ) ) { outlineDisable( outlineID, targetVeh ); } } LGM_targetFind() { targets = self maps\mp\gametypes\_weapons::lockOnLaunchers_getTargetArray(); targets = SortByDistance( targets, self.origin ); targetLook = undefined; foreach ( target in targets ) { if ( self WorldPointInReticle_Circle( target.origin, 65, 75 ) ) { targetLook = target; break; } } return targetLook; } // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // // LaserGuided Missile Utils // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. // LGM_enableLaser() { if ( !IsDefined( self.laserGuidedLauncher_laserOn ) || self.laserGuidedLauncher_laserOn == false ) { self.laserGuidedLauncher_laserOn = true; self enableWeaponLaser(); } } LGM_disableLaser() { if ( IsDefined( self.laserGuidedLauncher_laserOn ) && self.laserGuidedLauncher_laserOn == true ) { self disableWeaponLaser(); } self.laserGuidedLauncher_laserOn = undefined; } LGM_removeInvalidMissiles( missiles ) { valid = []; foreach ( m in missiles ) { if ( IsValidMissile( m ) ) { valid[ valid.size ] = m; } } return valid; } LGM_targetNotifyMissiles( targetVeh, attacker, missiles ) { // General notifies to other systems to handle incoming missiles level notify( "laserGuidedMissiles_incoming", attacker, missiles, targetVeh ); targetVeh notify( "targeted_by_incoming_missile", missiles ); } LGM_getTargetOffset( target ) { targetPoint = undefined; //AH: HACK: The harrier doesn't have the tag_missile_target, but it does have a tag_body. // The code works fine without this check, but GetTagOrigin throws an SRE if the tag does not exist. if ( target.model != "vehicle_av8b_harrier_jet_mp" ) targetPoint = target GetTagOrigin( "tag_missile_target" ); else targetPoint = target GetTagOrigin( "tag_body" ); if ( !IsDefined( targetPoint ) ) { targetPoint = target GetPointInBounds( 0, 0, 0 ); AssertMsg( "LGM_getTargetOffset() failed to find tag_missile_target on entity." + target.classname ); } return targetPoint - target.origin; } LGM_missilesNotifyAndRelease( entTarget ) { if ( IsDefined( entTarget.missilesChasing ) && entTarget.missilesChasing.size > 0 ) { foreach ( missChasing in entTarget.missilesChasing ) { if ( IsValidMissile( missChasing ) ) { // Let systems watching incoming missiles know the // target has changed missChasing notify( "missile_targetChanged" ); missChasing notify( "LGM_missile_abandoned" ); missChasing Missile_ClearTarget(); } } } entTarget.missilesChasing = []; }