2087 lines
52 KiB
Plaintext
2087 lines
52 KiB
Plaintext
#include animscripts\Utility;
|
|
#include maps\_gameskill;
|
|
#include maps\_utility;
|
|
#include common_scripts\utility;
|
|
#include animscripts\SetPoseMovement;
|
|
#using_animtree( "generic_human" );
|
|
|
|
|
|
getTargetAngleOffset( target )
|
|
{
|
|
pos = self getshootatpos() + ( 0, 0, -3 );// compensate for eye being higher than gun
|
|
dir = ( pos[ 0 ] - target[ 0 ], pos[ 1 ] - target[ 1 ], pos[ 2 ] - target[ 2 ] );
|
|
dir = VectorNormalize( dir );
|
|
fact = dir[ 2 ] * - 1;
|
|
// println ("offset " + fact);
|
|
return fact;
|
|
}
|
|
|
|
getSniperBurstDelayTime()
|
|
{
|
|
if ( isPlayer( self.enemy ) )
|
|
return randomFloatRange( self.enemy.gs.min_sniper_burst_delay_time, self.enemy.gs.max_sniper_burst_delay_time );
|
|
else
|
|
return randomFloatRange( anim.min_sniper_burst_delay_time, anim.max_sniper_burst_delay_time );
|
|
}
|
|
|
|
getRemainingBurstDelayTime()
|
|
{
|
|
timeSoFar = ( gettime() - self.a.lastShootTime ) / 1000;
|
|
delayTime = getBurstDelayTime();
|
|
if ( delayTime > timeSoFar )
|
|
return delayTime - timeSoFar;
|
|
return 0;
|
|
}
|
|
|
|
getBurstDelayTime()
|
|
{
|
|
if ( self usingSidearm() )
|
|
return randomFloatRange( .15, .55 );
|
|
else if ( weapon_pump_action_shotgun() )
|
|
return randomFloatRange( 1.0, 1.7 );
|
|
else if ( self isSniper() )
|
|
return getSniperBurstDelayTime();
|
|
else if ( self.fastBurst )
|
|
return randomFloatRange( .1, .35 );
|
|
else
|
|
return randomFloatRange( .4, .9 );
|
|
}
|
|
|
|
burstDelay()
|
|
{
|
|
if ( self.bulletsInClip )
|
|
{
|
|
if ( self.shootStyle == "full" && !self.fastBurst )
|
|
{
|
|
if ( self.a.lastShootTime == gettime() )
|
|
wait .05;
|
|
return;
|
|
}
|
|
|
|
delayTime = getRemainingBurstDelayTime();
|
|
if ( delayTime )
|
|
wait delayTime;
|
|
}
|
|
}
|
|
|
|
cheatAmmoIfNecessary()
|
|
{
|
|
assert( !self.bulletsInClip );
|
|
|
|
if ( !isdefined( self.enemy ) )
|
|
return false;
|
|
|
|
if ( self.team != "allies" )
|
|
{
|
|
// cheat and finish off the player if we can.
|
|
if ( !isPlayer( self.enemy ) )
|
|
return false;
|
|
//if ( self.enemy.health > self.enemy.maxHealth * level.healthOverlayCutoff )
|
|
// return false;
|
|
|
|
if ( self.enemy ent_flag( "player_is_invulnerable" ) )
|
|
return false;
|
|
}
|
|
|
|
if ( usingSidearm() || usingRocketLauncher() )
|
|
return false;
|
|
|
|
if ( gettime() - self.ammoCheatTime < self.ammoCheatInterval )
|
|
return false;
|
|
|
|
if ( !self canSee( self.enemy ) && distanceSquared( self.origin, self.enemy.origin ) > 256*256 )
|
|
return false;
|
|
|
|
self.bulletsInClip = int( weaponClipSize( self.weapon ) / 2 );
|
|
|
|
if ( self.bulletsInClip > weaponClipSize( self.weapon ) )
|
|
self.bulletsInClip = weaponClipSize( self.weapon );
|
|
|
|
self.ammoCheatTime = gettime();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
dontShoot_totalTime = 3;
|
|
dontShoot_interval = 0.05;
|
|
dontShoot_loopCount = dontShoot_totalTime / dontShoot_interval;
|
|
|
|
aimButDontShoot()
|
|
{
|
|
loopCount = int( dontShoot_loopCount );
|
|
|
|
while ( loopCount > 0 )
|
|
{
|
|
assert( !isdefined( self.dontEverShoot ) || self.dontEverShoot != 0 );
|
|
|
|
if ( isdefined( self.dontEverShoot ) || ( isdefined( self.enemy ) && isdefined( self.enemy.dontAttackMe ) ) )
|
|
wait dontShoot_interval;
|
|
else
|
|
return false;
|
|
|
|
loopCount--;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
shootUntilShootBehaviorChange()
|
|
{
|
|
self endon( "shoot_behavior_change" );
|
|
self endon( "stopShooting" );
|
|
|
|
if ( self isLongRangeAI() )
|
|
{
|
|
if ( isDefined( self.enemy ) && isAI( self.enemy ) && distanceSquared( level.player.origin, self.enemy.origin ) < 384 * 384 )
|
|
self.enemy animscripts\battlechatter_ai::addThreatEvent( "infantry", self, 1.0 );
|
|
|
|
if ( usingRocketLauncher() && isSentient( self.enemy ) )
|
|
wait( randomFloat( 2.0 ) );
|
|
}
|
|
|
|
if ( isdefined( self.enemy ) && distanceSquared( self.origin, self.enemy.origin ) > squared( 400 ) )
|
|
burstCount = randomintrange( 1, 5 );
|
|
else
|
|
burstCount = 10;
|
|
|
|
while ( 1 )
|
|
{
|
|
burstDelay();// waits only if necessary
|
|
|
|
// TODO: This sort of logic should really be in shoot_behavior. This thread is meant to be slave to shootent, shootpos, and shootstyle.
|
|
if ( aimButDontShoot() )
|
|
break;
|
|
|
|
if ( self.shootStyle == "full" )
|
|
{
|
|
// TODO: get rid of 'stopOnAnimationEnd', makes autofire not work if not enough fire notetracks
|
|
self FireUntilOutOfAmmo( animArray( "fire" ), true, animscripts\shared::decideNumShotsForFull() );
|
|
}
|
|
else if ( self.shootStyle == "burst" || self.shootStyle == "semi" )
|
|
{
|
|
numShots = animscripts\shared::decideNumShotsForBurst();
|
|
|
|
if ( numShots == 1 )
|
|
self FireUntilOutOfAmmo( animArrayPickRandom( "single" ), true, numShots );
|
|
else
|
|
self FireUntilOutOfAmmo( animArray( self.shootStyle + numShots ), true, numShots );
|
|
}
|
|
else if ( self.shootStyle == "single" )
|
|
{
|
|
self FireUntilOutOfAmmo( animArrayPickRandom( "single" ), true, 1 );
|
|
}
|
|
else
|
|
{
|
|
assert( self.shootStyle == "none" );
|
|
self waittill( "hell freezes over" );// waits for the endons to happen
|
|
}
|
|
|
|
if ( !self.bulletsInClip )
|
|
break;
|
|
|
|
burstCount--;
|
|
if ( burstCount < 0 )
|
|
{
|
|
self.shouldReturnToCover = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
getUniqueFlagNameIndex()
|
|
{
|
|
anim.animFlagNameIndex++;
|
|
return anim.animFlagNameIndex;
|
|
}
|
|
|
|
setupAim( transTime )
|
|
{
|
|
assert( isDefined( transTime ) );
|
|
|
|
self setAnim( %exposed_aiming, 1, .2 );
|
|
self setAnimKnobLimited( animarray( "straight_level" ), 1, transTime );
|
|
self setAnimKnobLimited( animArray( "add_aim_up" ), 1, transTime );
|
|
self setAnimKnobLimited( animArray( "add_aim_down" ), 1, transTime );
|
|
self setAnimKnobLimited( animArray( "add_aim_left" ), 1, transTime );
|
|
self setAnimKnobLimited( animArray( "add_aim_right" ), 1, transTime );
|
|
}
|
|
|
|
startFireAndAimIdleThread()
|
|
{
|
|
if ( !isdefined( self.a.aimIdleThread ) )
|
|
{
|
|
setupAim( 0.2 );
|
|
self thread aimIdleThread();
|
|
self thread animscripts\shared::trackShootEntOrPos();
|
|
}
|
|
}
|
|
|
|
endFireAndAnimIdleThread()
|
|
{
|
|
endAimIdleThread();
|
|
self clearAnim( %add_fire, .1 );
|
|
self notify( "stop tracking" );
|
|
}
|
|
|
|
showFireHideAimIdle()
|
|
{
|
|
if ( isdefined( self.a.aimIdleThread ) )
|
|
self setAnim( %add_idle, 0, .2 );
|
|
|
|
self setAnim( %add_fire, 1, .1 );
|
|
}
|
|
|
|
hideFireShowAimIdle()
|
|
{
|
|
if ( isdefined( self.a.aimIdleThread ) )
|
|
self setAnim( %add_idle, 1, .2 );
|
|
|
|
self setAnim( %add_fire, 0, .1 );
|
|
}
|
|
|
|
aimIdleThread( lean )
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "end_aim_idle_thread" );
|
|
|
|
if ( isdefined( self.a.aimIdleThread ) )
|
|
return;
|
|
self.a.aimIdleThread = true;
|
|
|
|
// wait a bit before starting idle since firing will end the idle thread
|
|
wait 0.1;
|
|
|
|
// this used to be setAnim, but it caused problems with turning on its parent nodes when they were supposed to be off (like during pistol pullout).
|
|
self setAnimLimited( %add_idle, 1, .2 );
|
|
|
|
for ( i = 0; ; i++ )
|
|
{
|
|
flagname = "idle" + i;
|
|
|
|
if ( isdefined( self.a.leanAim ) )
|
|
idleanim = animArrayPickRandom( "lean_idle" );
|
|
else
|
|
idleanim = animArrayPickRandom( "exposed_idle" );
|
|
|
|
self setFlaggedAnimKnobLimitedRestart( flagname, idleanim, 1, 0.2 );
|
|
|
|
self waittillmatch( flagname, "end" );
|
|
}
|
|
|
|
self clearAnim( %add_idle, .1 );
|
|
}
|
|
|
|
endAimIdleThread()
|
|
{
|
|
self notify( "end_aim_idle_thread" );
|
|
self.a.aimIdleThread = undefined;
|
|
self clearAnim( %add_idle, .1 );
|
|
}
|
|
|
|
shotgunFireRate()
|
|
{
|
|
if ( weapon_pump_action_shotgun() )
|
|
return 1.0;
|
|
|
|
if ( animscripts\weaponList::usingAutomaticWeapon() )
|
|
return animscripts\weaponList::autoShootAnimRate() * 0.7;
|
|
|
|
return 0.4;
|
|
}
|
|
|
|
FireUntilOutOfAmmo( fireAnim, stopOnAnimationEnd, maxshots )
|
|
{
|
|
animName = "fireAnim_" + getUniqueFlagNameIndex();
|
|
|
|
//prof_begin("FireUntilOutOfAmmo");
|
|
|
|
// reset our accuracy as we aim
|
|
maps\_gameskill::resetMissTime();
|
|
|
|
// first, wait until we're aimed right
|
|
while ( !aimedAtShootEntOrPos() )
|
|
wait .05;
|
|
|
|
//prof_begin("FireUntilOutOfAmmo");
|
|
self showFireHideAimIdle();
|
|
|
|
rate = 1.0;
|
|
if ( isdefined( self.shootRateOverride ) )
|
|
rate = self.shootRateOverride;
|
|
else if ( self.shootStyle == "full" )
|
|
rate = animscripts\weaponList::autoShootAnimRate() * randomfloatrange( 0.5, 1.0 );
|
|
else if ( self.shootStyle == "burst" )
|
|
rate = animscripts\weaponList::burstShootAnimRate();
|
|
else if ( usingSidearm() )
|
|
rate = 3.0;
|
|
else if ( usingShotgun() )
|
|
rate = shotgunFireRate();
|
|
|
|
self setFlaggedAnimKnobRestart( animName, fireAnim, 1, .2, rate );
|
|
|
|
// Update the sight accuracy against the player. Should be called before the volley starts.
|
|
self updatePlayerSightAccuracy();
|
|
|
|
//prof_end("FireUntilOutOfAmmo");
|
|
|
|
FireUntilOutOfAmmoInternal( animName, fireAnim, stopOnAnimationEnd, maxshots );
|
|
|
|
self hideFireShowAimIdle();
|
|
}
|
|
|
|
FireUntilOutOfAmmoInternal( animName, fireAnim, stopOnAnimationEnd, maxshots )
|
|
{
|
|
self endon( "enemy" );// stop shooting if our enemy changes, because we have to reset our accuracy and stuff
|
|
// stop shooting if the player becomes invulnerable, so we will call resetAccuracyAndPause again
|
|
if ( isPlayer( self.enemy ) && ( self.shootStyle == "full" || self.shootStyle == "semi" ) )
|
|
level endon( "player_becoming_invulnerable" );
|
|
|
|
if ( stopOnAnimationEnd )
|
|
{
|
|
self thread NotifyOnAnimEnd( animName, "fireAnimEnd" );
|
|
self endon( "fireAnimEnd" );
|
|
}
|
|
|
|
if ( !isdefined( maxshots ) )
|
|
maxshots = -1;
|
|
|
|
numshots = 0;
|
|
|
|
hasFireNotetrack = animHasNoteTrack( fireAnim, "fire" );
|
|
|
|
usingRocketLauncher = ( weaponClass( self.weapon ) == "rocketlauncher" );
|
|
|
|
while ( numshots < maxshots && maxshots > 0 ) // note: maxshots == -1 if no limit
|
|
{
|
|
//prof_begin("FireUntilOutOfAmmoInternal");
|
|
|
|
if ( hasFireNotetrack )
|
|
self waittillmatch( animName, "fire" );
|
|
|
|
if ( !self.bulletsInClip )
|
|
{
|
|
if ( !cheatAmmoIfNecessary() )
|
|
break;
|
|
}
|
|
|
|
if ( !aimedAtShootEntOrPos() )
|
|
break;
|
|
|
|
self shootAtShootEntOrPos();
|
|
|
|
assertex( self.bulletsInClip >= 0, self.bulletsInClip );
|
|
if ( isPlayer( self.enemy ) && self.enemy ent_flag( "player_is_invulnerable" ) )
|
|
{
|
|
if ( randomint( 3 ) == 0 )
|
|
self.bulletsInClip -- ;
|
|
}
|
|
else
|
|
{
|
|
self.bulletsInClip -- ;
|
|
}
|
|
|
|
if ( usingRocketLauncher )
|
|
{
|
|
self.a.rockets -- ;
|
|
if ( self.weapon == "rpg" )
|
|
{
|
|
self hidepart( "tag_rocket" );
|
|
self.a.rocketVisible = false;
|
|
}
|
|
}
|
|
|
|
numshots++;
|
|
|
|
self thread shotgunPumpSound( animName );
|
|
|
|
if ( self.fastBurst && numshots == maxshots )
|
|
break;
|
|
|
|
//prof_end("FireUntilOutOfAmmoInternal");
|
|
|
|
if ( !hasFireNotetrack || (maxShots == 1 && self.shootStyle == "single") )
|
|
self waittillmatch( animName, "end" );
|
|
}
|
|
|
|
if ( stopOnAnimationEnd )
|
|
self notify( "fireAnimEnd" );// stops NotifyOnAnimEnd()
|
|
}
|
|
|
|
aimedAtShootEntOrPos()
|
|
{
|
|
//prof_begin( "aimedAtShootEntOrPos" );
|
|
if ( !isdefined( self.shootPos ) )
|
|
{
|
|
assert( !isdefined( self.shootEnt ) );
|
|
|
|
//prof_end( "aimedAtShootEntOrPos" );
|
|
return true;
|
|
}
|
|
|
|
weaponAngles = self getMuzzleAngle();
|
|
shootFromPos = animscripts\shared::getShootFromPos();
|
|
|
|
anglesToShootPos = vectorToAngles( self.shootPos - shootFromPos );
|
|
|
|
absyawdiff = AbsAngleClamp180( weaponAngles[ 1 ] - anglesToShootPos[ 1 ] );
|
|
if ( absyawdiff > anim.aimYawDiffFarTolerance )
|
|
{
|
|
if ( distanceSquared( self getEye(), self.shootPos ) > anim.aimYawDiffCloseDistSQ || absyawdiff > anim.aimYawDiffCloseTolerance )
|
|
{
|
|
//prof_end( "aimedAtShootEntOrPos" );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//prof_end( "aimedAtShootEntOrPos" );
|
|
return AbsAngleClamp180( weaponAngles[ 0 ] - anglesToShootPos[ 0 ] ) <= anim.aimPitchDiffTolerance;
|
|
}
|
|
|
|
NotifyOnAnimEnd( animNotify, endNotify )
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( endNotify );
|
|
self waittillmatch( animNotify, "end" );
|
|
self notify( endNotify );
|
|
}
|
|
|
|
shootAtShootEntOrPos()
|
|
{
|
|
//prof_begin("shootAtShootEntOrPos");
|
|
|
|
if ( isdefined( self.shootEnt ) )
|
|
{
|
|
if ( isDefined( self.enemy ) && self.shootEnt == self.enemy )
|
|
self shootEnemyWrapper();
|
|
|
|
// it's possible that shootEnt isn't our enemy, which was probably caused by our enemy changing but shootEnt not being updated yet.
|
|
// we don't want to shoot directly at shootEnt because if our accuracy is 0 we shouldn't hit it perfectly.
|
|
// In retrospect, the existance of self.shootEnt was a bad idea and self.enemy should probably have just been used.
|
|
//else
|
|
// self shootPosWrapper( self.shootEnt getShootAtPos() );
|
|
}
|
|
else
|
|
{
|
|
// if self.shootPos isn't defined, "shoot_behavior_change" should
|
|
// have been notified and we shouldn't be firing anymore
|
|
assert( isdefined( self.shootPos ) );
|
|
|
|
self shootPosWrapper( self.shootPos );
|
|
}
|
|
|
|
//prof_end("shootAtShootEntOrPos");
|
|
}
|
|
|
|
showRocket()
|
|
{
|
|
if ( self.weapon != "rpg" )
|
|
return;
|
|
|
|
self.a.rocketVisible = true;
|
|
self showpart("tag_rocket");
|
|
self notify( "showing_rocket" );
|
|
}
|
|
|
|
showRocketWhenReloadIsDone()
|
|
{
|
|
if ( self.weapon != "rpg" )
|
|
return;
|
|
|
|
self endon( "death" );
|
|
self endon( "showing_rocket" );
|
|
self waittill( "killanimscript" );
|
|
|
|
self showRocket();
|
|
}
|
|
|
|
decrementBulletsInClip()
|
|
{
|
|
// we allow this to happen even when bulletsinclip is zero,
|
|
// because sometimes we want to shoot even if we're out of ammo,
|
|
// like when we've already started a blind fire animation.
|
|
if ( self.bulletsInClip )
|
|
self.bulletsInClip -- ;
|
|
}
|
|
|
|
shotgunPumpSound( animName )
|
|
{
|
|
if ( !weapon_pump_action_shotgun() )
|
|
return;
|
|
|
|
self endon( "killanimscript" );
|
|
|
|
self notify( "shotgun_pump_sound_end" );
|
|
self endon( "shotgun_pump_sound_end" );
|
|
|
|
self thread stopShotgunPumpAfterTime( 2.0 );
|
|
|
|
self waittillmatch( animName, "rechamber" );
|
|
|
|
self playSound( "ai_shotgun_pump" );
|
|
|
|
self notify( "shotgun_pump_sound_end" );
|
|
}
|
|
|
|
stopShotgunPumpAfterTime( timer )
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "shotgun_pump_sound_end" );
|
|
wait timer;
|
|
self notify( "shotgun_pump_sound_end" );
|
|
}
|
|
|
|
// Rechambers the weapon if appropriate
|
|
Rechamber( isExposed )
|
|
{
|
|
// obsolete...
|
|
}
|
|
|
|
// Returns true if character has less than thresholdFraction of his total bullets in his clip. Thus, a value
|
|
// of 1 would always reload, 0 would only reload on an empty clip.
|
|
NeedToReload( thresholdFraction )
|
|
{
|
|
if ( self.weapon == "none" )
|
|
return false;
|
|
|
|
if ( isdefined( self.noreload ) )
|
|
{
|
|
assertex( self.noreload, ".noreload must be true or undefined" );
|
|
if ( self.bulletsinclip < weaponClipSize( self.weapon ) * 0.5 )
|
|
self.bulletsinclip = int( weaponClipSize( self.weapon ) * 0.5 );
|
|
if ( self.bulletsinclip <= 0 )
|
|
self.bulletsinclip = 0;
|
|
return false;
|
|
}
|
|
|
|
if ( self.bulletsInClip <= weaponClipSize( self.weapon ) * thresholdFraction )
|
|
{
|
|
if ( thresholdFraction == 0 )
|
|
{
|
|
if ( cheatAmmoIfNecessary() )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Put the gun back in the AI's hand if he cuts off his weapon throw down animation
|
|
putGunBackInHandOnKillAnimScript()
|
|
{
|
|
self endon( "weapon_switch_done" );
|
|
self endon( "death" );
|
|
|
|
self waittill( "killanimscript" );
|
|
animscripts\shared::placeWeaponOn( self.primaryweapon, "right" );
|
|
}
|
|
|
|
Reload( thresholdFraction, optionalAnimation )
|
|
{
|
|
self endon( "killanimscript" );
|
|
|
|
if ( !NeedToReload( thresholdFraction ) )
|
|
return false;
|
|
|
|
//prof_begin( "Reload" );
|
|
|
|
self animscripts\battleChatter_ai::evaluateReloadEvent();
|
|
self animscripts\battleChatter::playBattleChatter();
|
|
|
|
if ( isDefined( optionalAnimation ) )
|
|
{
|
|
self setFlaggedAnimKnobAll( "reloadanim", optionalAnimation, %body, 1, .1, 1 );
|
|
animscripts\shared::DoNoteTracks( "reloadanim" );
|
|
self animscripts\weaponList::RefillClip(); // This should be in the animation as a notetrack in theory.
|
|
self.a.needsToRechamber = 0;
|
|
}
|
|
else
|
|
{
|
|
if ( self.a.pose == "prone" )
|
|
{
|
|
self setFlaggedAnimKnobAll( "reloadanim", %prone_reload, %body, 1, .1, 1 );
|
|
self UpdateProne( %prone_legs_up, %prone_legs_down, 1, 0.1, 1 );
|
|
}
|
|
else
|
|
{
|
|
println( "Bad anim_pose in combat::Reload" );
|
|
//prof_end( "Reload" );
|
|
wait 2;
|
|
return;
|
|
}
|
|
animscripts\shared::DoNoteTracks( "reloadanim" );
|
|
animscripts\weaponList::RefillClip(); // This should be in the animation as a notetrack in most instances.
|
|
self.a.needsToRechamber = 0;
|
|
}
|
|
|
|
//prof_end( "Reload" );
|
|
return true;
|
|
}
|
|
|
|
addGrenadeThrowAnimOffset( throwAnim, offset )
|
|
{
|
|
if ( !isdefined( anim.grenadeThrowAnims ) )
|
|
{
|
|
anim.grenadeThrowAnims = [];
|
|
anim.grenadeThrowOffsets = [];
|
|
}
|
|
|
|
assert( anim.grenadeThrowAnims.size == anim.grenadeThrowOffsets.size );
|
|
|
|
index = anim.grenadeThrowAnims.size;
|
|
anim.grenadeThrowAnims[ index ] = throwAnim;
|
|
anim.grenadeThrowOffsets[ index ] = offset;
|
|
}
|
|
|
|
initGrenadeThrowAnims()
|
|
{
|
|
// generated with scr_testgrenadethrows in combat.gsc
|
|
addGrenadeThrowAnimOffset( %exposed_grenadethrowb, ( 41.5391, 7.28883, 72.2128 ) );
|
|
addGrenadeThrowAnimOffset( %exposed_grenadethrowc, ( 34.8849, -4.77048, 74.0488 ) );
|
|
addGrenadeThrowAnimOffset( %corner_standl_grenade_a, ( 41.605, 6.80107, 81.4785 ) );
|
|
addGrenadeThrowAnimOffset( %corner_standl_grenade_b, ( 24.1585, -14.7221, 29.2992 ) );
|
|
addGrenadeThrowAnimOffset( %cornercrl_grenadea, ( 25.8988, -10.2811, 30.4813 ) );
|
|
addGrenadeThrowAnimOffset( %cornercrl_grenadeb, ( 24.688, 45.0702, 64.377 ) );
|
|
addGrenadeThrowAnimOffset( %corner_standr_grenade_a, ( 37.1254, -32.7053, 76.5745 ) );
|
|
addGrenadeThrowAnimOffset( %corner_standr_grenade_b, ( 19.356, 15.5341, 16.5036 ) );
|
|
addGrenadeThrowAnimOffset( %cornercrr_grenadea, ( 39.8857, 5.92472, 24.5878 ) );
|
|
addGrenadeThrowAnimOffset( %covercrouch_grenadea, ( -1.6363, -0.693674, 60.1009 ) );
|
|
addGrenadeThrowAnimOffset( %covercrouch_grenadeb, ( -1.6363, -0.693674, 60.1009 ) );
|
|
addGrenadeThrowAnimOffset( %coverstand_grenadea, ( 10.8573, 7.12614, 77.2356 ) );
|
|
addGrenadeThrowAnimOffset( %coverstand_grenadeb, ( 19.1804, 5.68214, 73.2278 ) );
|
|
addGrenadeThrowAnimOffset( %prone_grenade_a, ( 12.2859, -1.3019, 33.4307 ) );
|
|
addGrenadeThrowAnimOffset( %CQB_stand_grenade_throw, ( 35.7494, 26.6052, 37.7086 ) );
|
|
}
|
|
|
|
|
|
getGrenadeThrowOffset( throwAnim )
|
|
{
|
|
//prof_begin( "getGrenadeThrowOffset" );
|
|
offset = ( 0, 0, 64 );
|
|
|
|
if ( isdefined( throwAnim ) )
|
|
{
|
|
foreach( index, grenadeThrowAnim in anim.grenadeThrowAnims )
|
|
{
|
|
if ( throwAnim == grenadeThrowAnim )
|
|
{
|
|
offset = anim.grenadeThrowOffsets[ index ];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( offset[ 2 ] == 64 )
|
|
{
|
|
if ( isdefined( throwAnim ) )
|
|
println( "^1Warning: undefined grenade throw animation used; hand offset unknown" );
|
|
else
|
|
println( "^1Warning: grenade throw animation ", throwAnim, " has no recorded hand offset" );
|
|
}
|
|
|
|
//prof_end( "getGrenadeThrowOffset" );
|
|
return offset;
|
|
}
|
|
|
|
// this function is called from maps\_utility::ThrowGrenadeAtPlayerASAP
|
|
ThrowGrenadeAtPlayerASAP_combat_utility()
|
|
{
|
|
assert( self isBadGuy() );
|
|
|
|
for ( i = 0; i < level.players.size; i++ )
|
|
{
|
|
if ( level.players[ i ].numGrenadesInProgressTowardsPlayer == 0 )
|
|
{
|
|
level.players[ i ].grenadeTimers[ "fraggrenade" ] = 0;
|
|
level.players[ i ].grenadeTimers[ "flash_grenade" ] = 0;
|
|
}
|
|
}
|
|
anim.throwGrenadeAtPlayerASAP = true;
|
|
|
|
/#
|
|
enemies = getaiarray( "bad_guys" );
|
|
if ( enemies.size == 0 )
|
|
return;
|
|
numwithgrenades = 0;
|
|
for ( i = 0; i < enemies.size; i++ )
|
|
{
|
|
if ( enemies[ i ].grenadeammo > 0 )
|
|
return;
|
|
}
|
|
println( "^1Warning: called ThrowGrenadeAtPlayerASAP, but no enemies have any grenadeammo!" );
|
|
#/
|
|
}
|
|
|
|
setActiveGrenadeTimer( throwingAt )
|
|
{
|
|
self.activeGrenadeTimer = spawnstruct();
|
|
if ( isPlayer( throwingAt ) )
|
|
{
|
|
self.activeGrenadeTimer.isPlayerTimer = true;
|
|
self.activeGrenadeTimer.player = throwingAt;
|
|
self.activeGrenadeTimer.timerName = self.grenadeWeapon;
|
|
assertex( isdefined( throwingAt.grenadeTimers[ self.activeGrenadeTimer.timerName ] ), "No grenade timer for " + self.activeGrenadeTimer.timerName );
|
|
}
|
|
else
|
|
{
|
|
self.activeGrenadeTimer.isPlayerTimer = false;
|
|
self.activeGrenadeTimer.timerName = "AI_" + self.grenadeWeapon;
|
|
assertex( isdefined( anim.grenadeTimers[ self.activeGrenadeTimer.timerName ] ), "No grenade timer for " + self.activeGrenadeTimer.timerName );
|
|
}
|
|
}
|
|
|
|
usingPlayerGrenadeTimer()
|
|
{
|
|
assert( isDefined( self.activeGrenadeTimer ) );
|
|
return self.activeGrenadeTimer.isPlayerTimer;
|
|
}
|
|
|
|
setGrenadeTimer( grenadeTimer, newValue )
|
|
{
|
|
if ( grenadeTimer.isPlayerTimer )
|
|
{
|
|
player = grenadeTimer.player;
|
|
oldValue = player.grenadeTimers[ grenadeTimer.timerName ];
|
|
player.grenadeTimers[ grenadeTimer.timerName ] = max( newValue, oldValue );
|
|
}
|
|
else
|
|
{
|
|
oldValue = anim.grenadeTimers[ grenadeTimer.timerName ];
|
|
anim.grenadeTimers[ grenadeTimer.timerName ] = max( newValue, oldValue );
|
|
}
|
|
}
|
|
|
|
getDesiredGrenadeTimerValue()
|
|
{
|
|
nextGrenadeTimeToUse = undefined;
|
|
if ( self usingPlayerGrenadeTimer() )
|
|
{
|
|
player = self.activeGrenadeTimer.player;
|
|
nextGrenadeTimeToUse = gettime() + player.gs.playerGrenadeBaseTime + randomint( player.gs.playerGrenadeRangeTime );
|
|
}
|
|
else
|
|
{
|
|
nextGrenadeTimeToUse = gettime() + 30000 + randomint( 30000 );
|
|
}
|
|
return nextGrenadeTimeToUse;
|
|
}
|
|
|
|
getGrenadeTimerTime( grenadeTimer )
|
|
{
|
|
if ( grenadeTimer.isPlayerTimer )
|
|
{
|
|
return grenadeTimer.player.grenadeTimers[ grenadeTimer.timerName ];
|
|
}
|
|
else
|
|
{
|
|
return anim.grenadeTimers[ grenadeTimer.timerName ];
|
|
}
|
|
}
|
|
|
|
considerChangingTarget( throwingAt )
|
|
{
|
|
//prof_begin( "considerChangingTarget" );
|
|
|
|
if ( !isPlayer( throwingAt ) && self isBadGuy() )
|
|
{
|
|
if ( gettime() < getGrenadeTimerTime( self.activeGrenadeTimer ) )
|
|
{
|
|
if ( level.player.ignoreme )
|
|
{
|
|
//prof_end( "considerChangingTarget" );
|
|
return throwingAt;
|
|
}
|
|
|
|
// check if player threatbias is set to be ignored by self
|
|
myGroup = self getthreatbiasgroup();
|
|
playerGroup = level.player getthreatbiasgroup();
|
|
|
|
if ( myGroup != "" && playerGroup != "" && getThreatBias( playerGroup, myGroup ) < - 10000 )
|
|
{
|
|
//prof_end( "considerChangingTarget" );
|
|
return throwingAt;
|
|
}
|
|
|
|
|
|
// can't throw at an AI right now anyway.
|
|
// check if the player is an acceptable target (be careful not to be aware of him when we wouldn't know about him)
|
|
if ( self canSee( level.player ) || ( isAI( throwingAt ) && throwingAt canSee( level.player ) ) )
|
|
{
|
|
if ( isdefined( self.covernode ) )
|
|
{
|
|
angles = VectorToAngles( level.player.origin - self.origin );
|
|
yawDiff = AngleClamp180( self.covernode.angles[ 1 ] - angles[ 1 ] );
|
|
}
|
|
else
|
|
{
|
|
yawDiff = self GetYawToSpot( level.player.origin );
|
|
}
|
|
|
|
if ( abs( yawDiff ) < 60 )
|
|
{
|
|
throwingAt = level.player;
|
|
self setActiveGrenadeTimer( throwingAt );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//prof_end( "considerChangingTarget" );
|
|
return throwingAt;
|
|
}
|
|
|
|
// a "double" grenade is when 2 grenades land at the player's feet at once.
|
|
// we do this sometimes on harder difficulty modes.
|
|
mayThrowDoubleGrenade( throwingAt )
|
|
{
|
|
assert( self.activeGrenadeTimer.isPlayerTimer );
|
|
assert( self.activeGrenadeTimer.timerName == "fraggrenade" );
|
|
assert( isPlayer( throwingAt ) );
|
|
|
|
if ( player_died_recently() )
|
|
return false;
|
|
|
|
if ( !throwingAt.gs.double_grenades_allowed )
|
|
return false;
|
|
|
|
time = gettime();
|
|
|
|
// if it hasn't been long enough since the last double grenade, don't do it
|
|
if ( time < throwingAt.grenadeTimers[ "double_grenade" ] )
|
|
return false;
|
|
|
|
// if no one's started throwing a grenade recently, we can't do it
|
|
if ( time > throwingAt.lastFragGrenadeToPlayerStart + 3000 )
|
|
return false;
|
|
|
|
// stagger double grenades by 0.5 sec
|
|
if ( time < throwingAt.lastFragGrenadeToPlayerStart + 500 )
|
|
return false;
|
|
|
|
return throwingAt.numGrenadesInProgressTowardsPlayer < 2;
|
|
}
|
|
|
|
myGrenadeCoolDownElapsed()
|
|
{
|
|
// this should be as fast as possible; put slow checks in grenadeCoolDownElapsed
|
|
return( gettime() >= self.a.nextGrenadeTryTime );
|
|
}
|
|
|
|
grenadeCoolDownElapsed( throwingAt )
|
|
{
|
|
if ( player_died_recently() )
|
|
return false;
|
|
|
|
if ( self.script_forcegrenade == 1 )
|
|
return true;
|
|
|
|
if ( !myGrenadeCoolDownElapsed() )
|
|
return false;
|
|
|
|
if ( gettime() >= getGrenadeTimerTime( self.activeGrenadeTimer ) )
|
|
return true;
|
|
|
|
if ( self.activeGrenadeTimer.isPlayerTimer && self.activeGrenadeTimer.timerName == "fraggrenade" )
|
|
return mayThrowDoubleGrenade( throwingAt );
|
|
|
|
return false;
|
|
}
|
|
|
|
/#
|
|
getGrenadeTimerDebugName( grenadeTimer )
|
|
{
|
|
if ( grenadeTimer.isPlayerTimer )
|
|
{
|
|
for ( i = 0; i < level.players.size; i++ )
|
|
{
|
|
if ( level.players[ i ] == grenadeTimer.player )
|
|
break;
|
|
}
|
|
return "Player " + ( i + 1 ) + " " + grenadeTimer.timerName;
|
|
}
|
|
else
|
|
{
|
|
return "AI " + grenadeTimer.timerName;
|
|
}
|
|
}
|
|
|
|
printGrenadeTimers()
|
|
{
|
|
level notify( "stop_printing_grenade_timers" );
|
|
level endon( "stop_printing_grenade_timers" );
|
|
|
|
x = 40;
|
|
y = 40;
|
|
|
|
level.grenadeTimerHudElem = [];
|
|
|
|
level.grenadeDebugTimers = [];
|
|
keys = getArrayKeys( anim.grenadeTimers );
|
|
for ( i = 0; i < keys.size; i++ )
|
|
{
|
|
timer = spawnstruct();
|
|
timer.isPlayerTimer = false;
|
|
timer.timerName = keys[ i ];
|
|
level.grenadeDebugTimers[ level.grenadeDebugTimers.size ] = timer;
|
|
}
|
|
for ( i = 0; i < level.players.size; i++ )
|
|
{
|
|
player = level.players[ i ];
|
|
keys = getArrayKeys( player.grenadeTimers );
|
|
for ( j = 0; j < keys.size; j++ )
|
|
{
|
|
timer = spawnstruct();
|
|
timer.isPlayerTimer = true;
|
|
timer.player = player;
|
|
timer.timerName = keys[ j ];
|
|
level.grenadeDebugTimers[ level.grenadeDebugTimers.size ] = timer;
|
|
}
|
|
}
|
|
|
|
for ( i = 0; i < level.grenadeDebugTimers.size; i++ )
|
|
{
|
|
textelem = newHudElem();
|
|
textelem.x = x;
|
|
textelem.y = y;
|
|
textelem.alignX = "left";
|
|
textelem.alignY = "top";
|
|
textelem.horzAlign = "fullscreen";
|
|
textelem.vertAlign = "fullscreen";
|
|
textelem setText( getGrenadeTimerDebugName( level.grenadeDebugTimers[ i ] ) );
|
|
|
|
bar = newHudElem();
|
|
bar.x = x + 110;
|
|
bar.y = y + 2;
|
|
bar.alignX = "left";
|
|
bar.alignY = "top";
|
|
bar.horzAlign = "fullscreen";
|
|
bar.vertAlign = "fullscreen";
|
|
bar setshader( "black", 1, 8 );
|
|
|
|
textelem.bar = bar;
|
|
|
|
y += 10;
|
|
|
|
level.grenadeDebugTimers[ i ].textelem = textelem;
|
|
}
|
|
|
|
while ( 1 )
|
|
{
|
|
wait .05;
|
|
|
|
for ( i = 0; i < level.grenadeDebugTimers.size; i++ )
|
|
{
|
|
timeleft = ( getGrenadeTimerTime( level.grenadeDebugTimers[ i ] ) - gettime() ) / 1000;
|
|
|
|
width = max( timeleft * 4, 1 );
|
|
width = int( width );
|
|
|
|
bar = level.grenadeDebugTimers[ i ].textelem.bar;
|
|
bar setShader( "black", width, 8 );
|
|
}
|
|
}
|
|
}
|
|
|
|
destroyGrenadeTimers()
|
|
{
|
|
if ( !isdefined( level.grenadeDebugTimers ) )
|
|
return;
|
|
for ( i = 0; i < level.grenadeDebugTimers.size; i++ )
|
|
{
|
|
level.grenadeDebugTimers[ i ].textelem.bar destroy();
|
|
level.grenadeDebugTimers[ i ].textelem destroy();
|
|
}
|
|
level.grenadeDebugTimers = undefined;
|
|
}
|
|
|
|
grenadeTimerDebug()
|
|
{
|
|
setDvarIfUninitialized( "scr_grenade_debug", "0" );
|
|
|
|
while ( 1 )
|
|
{
|
|
while ( 1 )
|
|
{
|
|
if ( getdebugdvar( "scr_grenade_debug" ) == "1" )
|
|
break;
|
|
wait .5;
|
|
}
|
|
thread printGrenadeTimers();
|
|
while ( 1 )
|
|
{
|
|
if ( getdebugdvar( "scr_grenade_debug" ) != "1" )
|
|
break;
|
|
wait .5;
|
|
}
|
|
level notify( "stop_printing_grenade_timers" );
|
|
destroyGrenadeTimers();
|
|
}
|
|
}
|
|
|
|
grenadeDebug( state, duration, showMissReason )
|
|
{
|
|
if ( getdebugdvar( "scr_grenade_debug" ) != "1" )
|
|
return;
|
|
|
|
self notify( "grenade_debug" );
|
|
self endon( "grenade_debug" );
|
|
self endon( "killanimscript" );
|
|
self endon( "death" );
|
|
endtime = gettime() + 1000 * duration;
|
|
|
|
while ( gettime() < endtime )
|
|
{
|
|
print3d( self getShootAtPos() + ( 0, 0, 10 ), state );
|
|
if ( isdefined( showMissReason ) && isdefined( self.grenadeMissReason ) )
|
|
print3d( self getShootAtPos() + ( 0, 0, 0 ), "Failed: " + self.grenadeMissReason );
|
|
else if ( isdefined( self.activeGrenadeTimer ) )
|
|
print3d( self getShootAtPos() + ( 0, 0, 0 ), "Timer: " + getGrenadeTimerDebugName( self.activeGrenadeTimer ) );
|
|
wait .05;
|
|
}
|
|
}
|
|
|
|
setGrenadeMissReason( reason )
|
|
{
|
|
if ( getdebugdvar( "scr_grenade_debug" ) != "1" )
|
|
return;
|
|
self.grenadeMissReason = reason;
|
|
}
|
|
#/
|
|
|
|
TryGrenadePosProc( throwingAt, destination, optionalAnimation, armOffset )
|
|
{
|
|
// Dont throw a grenade right near you or your buddies
|
|
if ( !( self isGrenadePosSafe( throwingAt, destination ) ) )
|
|
return false;
|
|
else if ( distanceSquared( self.origin, destination ) < 200 * 200 )
|
|
return false;
|
|
|
|
//prof_begin( "TryGrenadePosProc" );
|
|
|
|
trace = physicsTrace( destination + ( 0, 0, 1 ), destination + ( 0, 0, -500 ) );
|
|
if ( trace == destination + ( 0, 0, -500 ) )
|
|
return false;
|
|
trace += ( 0, 0, .1 );// ensure just above ground
|
|
|
|
//prof_end( "TryGrenadePosProc" );
|
|
|
|
return TryGrenadeThrow( throwingAt, trace, optionalAnimation, armOffset );
|
|
}
|
|
|
|
TryGrenade( throwingAt, optionalAnimation )
|
|
{
|
|
if ( self.weapon == "mg42" || self.grenadeammo <= 0 )
|
|
return false;
|
|
|
|
self setActiveGrenadeTimer( throwingAt );
|
|
|
|
throwingAt = considerChangingTarget( throwingAt );
|
|
|
|
if ( !grenadeCoolDownElapsed( throwingAt ) )
|
|
return false;
|
|
|
|
/#
|
|
self thread grenadeDebug( "Tried grenade throw", 4, true );
|
|
#/
|
|
|
|
armOffset = getGrenadeThrowOffset( optionalAnimation );
|
|
|
|
if ( isdefined( self.enemy ) && throwingAt == self.enemy )
|
|
{
|
|
if ( !checkGrenadeThrowDist() )
|
|
{
|
|
/# self setGrenadeMissReason( "Too close or too far" ); #/
|
|
return false;
|
|
}
|
|
|
|
if ( isPlayer( self.enemy ) && self.enemy isPlayerDown() )
|
|
{
|
|
/# self setGrenadeMissReason( "Enemy is downed player" ); #/
|
|
return false;
|
|
}
|
|
|
|
if ( self canSeeEnemyFromExposed() )
|
|
{
|
|
if ( !( self isGrenadePosSafe( throwingAt, throwingAt.origin ) ) )
|
|
{
|
|
/# self setGrenadeMissReason( "Teammates near target" ); #/
|
|
return false;
|
|
}
|
|
return TryGrenadeThrow( throwingAt, undefined, optionalAnimation, armOffset );
|
|
}
|
|
else if ( self canSuppressEnemyFromExposed() )
|
|
{
|
|
return TryGrenadePosProc( throwingAt, self getEnemySightPos(), optionalAnimation, armOffset );
|
|
}
|
|
else
|
|
{
|
|
// hopefully we can get through a grenade hint or something
|
|
if ( !( self isGrenadePosSafe( throwingAt, throwingAt.origin ) ) )
|
|
{
|
|
/# self setGrenadeMissReason( "Teammates near target" ); #/
|
|
return false;
|
|
}
|
|
return TryGrenadeThrow( throwingAt, undefined, optionalAnimation, armOffset );
|
|
}
|
|
|
|
/# self setGrenadeMissReason( "Don't know where to throw" ); #/
|
|
return false;// didn't know where to throw!
|
|
}
|
|
else
|
|
{
|
|
return TryGrenadePosProc( throwingAt, throwingAt.origin, optionalAnimation, armOffset );
|
|
}
|
|
}
|
|
|
|
TryGrenadeThrow( throwingAt, destination, optionalAnimation, armOffset, fastThrow, withBounce, throwInThread )
|
|
{
|
|
// no AI grenade throws in the first 10 seconds, bad during black screen
|
|
if ( gettime() < 10000 && !isdefined( level.ignoreGrenadeSafeTime ) )
|
|
{
|
|
/# self setGrenadeMissReason( "First 10 seconds of game" ); #/
|
|
return false;
|
|
}
|
|
|
|
if ( !isdefined( withBounce ) )
|
|
withBounce = true;
|
|
|
|
//prof_begin( "TryGrenadeThrow" );
|
|
|
|
if ( isDefined( optionalAnimation ) )
|
|
{
|
|
throw_anim = optionalAnimation;
|
|
// Assume armOffset and gunHand are defined whenever optionalAnimation is.
|
|
gunHand = self.a.gunHand; // Actually we don't want gunhand in this case. We rely on notetracks.
|
|
}
|
|
else
|
|
{
|
|
switch( self.a.special )
|
|
{
|
|
case "cover_crouch":
|
|
case "none":
|
|
if ( self.a.pose == "stand" )
|
|
{
|
|
armOffset = ( 0, 0, 80 );
|
|
throw_anim = %stand_grenade_throw;
|
|
}
|
|
else// if ( self.a.pose == "crouch" )
|
|
{
|
|
armOffset = ( 0, 0, 65 );
|
|
throw_anim = %crouch_grenade_throw;
|
|
}
|
|
gunHand = "left";
|
|
break;
|
|
default:// Do nothing - we don't have an appropriate throw animation.
|
|
throw_anim = undefined;
|
|
gunHand = undefined;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we don't have an animation, we can't throw the grenade.
|
|
if ( !isDefined( throw_anim ) )
|
|
{
|
|
//prof_end( "TryGrenadeThrow" );
|
|
return( false );
|
|
}
|
|
|
|
if ( isdefined( destination ) )// Now try to throw it.
|
|
{
|
|
if ( !isdefined( fastThrow ) )
|
|
throwvel = self checkGrenadeThrowPos( armOffset, destination, withBounce, "min energy", "min time", "max time" );
|
|
else
|
|
throwvel = self checkGrenadeThrowPos( armOffset, destination, withBounce, "min time", "min energy" );
|
|
}
|
|
else
|
|
{
|
|
randomRange = self.randomGrenadeRange;
|
|
// scale down random range as target gets closer to avoid crazy sideways throws
|
|
dist = distance( throwingAt.origin, self.origin );
|
|
if ( dist < 800 )
|
|
{
|
|
if ( dist < 256 )
|
|
randomRange = 0;
|
|
else
|
|
randomRange *= (dist - 256) / (800 - 256);
|
|
}
|
|
|
|
assert( self.enemy == throwingAt );
|
|
if ( !isdefined( fastThrow ) )
|
|
throwvel = self checkGrenadeThrow( armOffset, randomRange, "min energy", "min time", "max time" );
|
|
else
|
|
throwvel = self checkGrenadeThrow( armOffset, randomRange, "min time", "min energy" );
|
|
}
|
|
|
|
// the grenade checks are slow. don't do it too often.
|
|
self.a.nextGrenadeTryTime = gettime() + randomintrange( 1000, 2000 );
|
|
|
|
if ( isdefined( throwvel ) )
|
|
{
|
|
if ( !isdefined( self.oldGrenAwareness ) )
|
|
self.oldGrenAwareness = self.grenadeawareness;
|
|
self.grenadeawareness = 0;// so we dont respond to nearby grenades while throwing one
|
|
|
|
/#
|
|
if ( getdebugdvar( "anim_debug" ) == "1" )
|
|
thread animscripts\utility::debugPos( destination, "O" );
|
|
#/
|
|
|
|
// remember the time we want to delay any future grenade throws to, to avoid throwing too many.
|
|
// however, for now, only set the timer far enough in the future that it will expire when we throw the grenade.
|
|
// that way, if the throw fails (maybe due to killanimscript), we'll try again soon.
|
|
nextGrenadeTimeToUse = self getDesiredGrenadeTimerValue();
|
|
setGrenadeTimer( self.activeGrenadeTimer, min( gettime() + 3000, nextGrenadeTimeToUse ) );
|
|
|
|
secondGrenadeOfDouble = false;
|
|
if ( self usingPlayerGrenadeTimer() )
|
|
{
|
|
assert( throwingAt == self.activeGrenadeTimer.player );
|
|
throwingAt.numGrenadesInProgressTowardsPlayer++ ;
|
|
self thread reduceGIPTPOnKillanimscript( throwingAt );
|
|
if ( throwingAt.numGrenadesInProgressTowardsPlayer > 1 )
|
|
secondGrenadeOfDouble = true;
|
|
|
|
if ( self.activeGrenadeTimer.timerName == "fraggrenade" )
|
|
{
|
|
if ( throwingAt.numGrenadesInProgressTowardsPlayer <= 1 )
|
|
throwingAt.lastFragGrenadeToPlayerStart = gettime();
|
|
}
|
|
}
|
|
|
|
/#
|
|
if ( getdvar( "grenade_spam" ) == "on" )
|
|
nextGrenadeTimeToUse = 0;
|
|
#/
|
|
|
|
//prof_end( "TryGrenadeThrow" );
|
|
if ( isdefined( throwInThread ) )
|
|
thread DoGrenadeThrow( throw_anim, throwVel, nextGrenadeTimeToUse, secondGrenadeOfDouble );
|
|
else
|
|
DoGrenadeThrow( throw_anim, throwVel, nextGrenadeTimeToUse, secondGrenadeOfDouble );
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
/# self setGrenadeMissReason( "Couldn't find trajectory" ); #/
|
|
/#
|
|
if ( getdebugdvar( "debug_grenademiss" ) == "on" && isdefined( destination ) )
|
|
thread grenadeLine( armoffset, destination );
|
|
#/
|
|
}
|
|
|
|
//prof_end( "TryGrenadeThrow" );
|
|
return false;
|
|
}
|
|
|
|
reduceGIPTPOnKillanimscript( throwingAt )
|
|
{
|
|
self endon( "dont_reduce_giptp_on_killanimscript" );
|
|
self waittill( "killanimscript" );
|
|
throwingAt.numGrenadesInProgressTowardsPlayer -- ;
|
|
}
|
|
|
|
DoGrenadeThrow( throw_anim, throwVel, nextGrenadeTimeToUse, secondGrenadeOfDouble )
|
|
{
|
|
self endon( "killanimscript" );
|
|
/#
|
|
self thread grenadeDebug( "Starting throw", 3 );
|
|
#/
|
|
|
|
//prof_begin( "DoGrenadeThrow" );
|
|
|
|
if ( self.script == "combat" || self.script == "move" )
|
|
self orientmode( "face direction", throwVel );
|
|
|
|
self animscripts\battleChatter_ai::evaluateAttackEvent( self.grenadeWeapon );
|
|
self notify( "stop_aiming_at_enemy" );
|
|
self SetFlaggedAnimKnobAllRestart( "throwanim", throw_anim, %body, fasterAnimSpeed(), 0.1, 1 );
|
|
|
|
self thread animscripts\shared::DoNoteTracksForever( "throwanim", "killanimscript" );
|
|
|
|
//prof_begin( "DoGrenadeThrow" );
|
|
|
|
model = getGrenadeModel();
|
|
|
|
attachside = "none";
|
|
for ( ;; )
|
|
{
|
|
self waittill( "throwanim", notetrack );
|
|
//prof_begin( "DoGrenadeThrow" );
|
|
if ( notetrack == "grenade_left" || notetrack == "grenade_right" )
|
|
{
|
|
attachside = attachGrenadeModel( model, "TAG_INHAND" );
|
|
self.isHoldingGrenade = true;
|
|
}
|
|
if ( notetrack == "grenade_throw" || notetrack == "grenade throw" )
|
|
break;
|
|
assert( notetrack != "end" );// we shouldn't hit "end" until after we've hit "grenade_throw"!
|
|
if ( notetrack == "end" )// failsafe
|
|
{
|
|
self.activeGrenadeTimer.player.numGrenadesInProgressTowardsPlayer -- ;
|
|
self notify( "dont_reduce_giptp_on_killanimscript" );
|
|
//prof_end( "DoGrenadeThrow" );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/#
|
|
if ( getdebugdvar( "debug_grenadehand" ) == "on" )
|
|
{
|
|
tags = [];
|
|
numTags = self getAttachSize();
|
|
emptySlot = [];
|
|
for ( i = 0;i < numTags;i++ )
|
|
{
|
|
name = self getAttachModelName( i );
|
|
if ( issubstr( name, "weapon" ) )
|
|
{
|
|
tagName = self getAttachTagname( i );
|
|
emptySlot[ tagname ] = 0;
|
|
tags[ tags.size ] = tagName;
|
|
}
|
|
}
|
|
|
|
for ( i = 0;i < tags.size;i++ )
|
|
{
|
|
emptySlot[ tags[ i ] ]++ ;
|
|
if ( emptySlot[ tags[ i ] ] < 2 )
|
|
continue;
|
|
iprintlnbold( "Grenade throw needs fixing (check console)" );
|
|
println( "Grenade throw animation ", throw_anim, " has multiple weapons attached to ", tags[ i ] );
|
|
break;
|
|
}
|
|
}
|
|
#/
|
|
|
|
/#
|
|
self thread grenadeDebug( "Threw", 5 );
|
|
#/
|
|
|
|
self notify( "dont_reduce_giptp_on_killanimscript" );
|
|
|
|
if ( self usingPlayerGrenadeTimer() )
|
|
{
|
|
// give the grenade some time to get to the player.
|
|
// if it gets there, we'll reset the timer so we don't throw any more in a while.
|
|
self thread watchGrenadeTowardsPlayer( self.activeGrenadeTimer.player, nextGrenadeTimeToUse );
|
|
}
|
|
|
|
self throwGrenade();
|
|
|
|
|
|
if ( !self usingPlayerGrenadeTimer() )
|
|
{
|
|
setGrenadeTimer( self.activeGrenadeTimer, nextGrenadeTimeToUse );
|
|
}
|
|
|
|
if ( secondGrenadeOfDouble )
|
|
{
|
|
assert( self.activeGrenadeTimer.isPlayerTimer );
|
|
player = self.activeGrenadeTimer.player;
|
|
assert( isPlayer( player ) );
|
|
if ( player.numGrenadesInProgressTowardsPlayer > 1 || gettime() - player.lastGrenadeLandedNearPlayerTime < 2000 )
|
|
{
|
|
// two grenades in progress toward player. give them time to arrive.
|
|
player.grenadeTimers[ "double_grenade" ] = gettime() + min( 5000, player.gs.playerDoubleGrenadeTime );
|
|
}
|
|
}
|
|
|
|
self notify( "stop grenade check" );
|
|
|
|
// assert (attachSide != "none");
|
|
if ( attachSide != "none" )
|
|
self detach( model, attachside );
|
|
else
|
|
{
|
|
print( "No grenade hand set: " );
|
|
println( throw_anim );
|
|
println( "animation in console does not specify grenade hand" );
|
|
}
|
|
self.isHoldingGrenade = undefined;
|
|
|
|
self.grenadeawareness = self.oldGrenAwareness;
|
|
self.oldGrenAwareness = undefined;
|
|
|
|
//prof_end( "DoGrenadeThrow" );
|
|
|
|
self waittillmatch( "throwanim", "end" );
|
|
self notify( "done_grenade_throw" );
|
|
self notify( "weapon_switch_done" );
|
|
// modern
|
|
|
|
// TODO: why is this here? why are we assuming that the calling function wants these particular animnodes turned on?
|
|
self setanim( %exposed_modern, 1, .2 );
|
|
self setanim( %exposed_aiming, 1 );
|
|
self clearanim( throw_anim, .2 );
|
|
}
|
|
|
|
watchGrenadeTowardsPlayer( player, nextGrenadeTimeToUse )
|
|
{
|
|
player endon( "death" );
|
|
|
|
watchGrenadeTowardsPlayerInternal( nextGrenadeTimeToUse );
|
|
player.numGrenadesInProgressTowardsPlayer -- ;
|
|
}
|
|
|
|
watchGrenadeTowardsPlayerInternal( nextGrenadeTimeToUse )
|
|
{
|
|
// give the grenade at least 5 seconds to land
|
|
activeGrenadeTimer = self.activeGrenadeTimer;
|
|
timeoutObj = spawnstruct();
|
|
timeoutObj thread watchGrenadeTowardsPlayerTimeout( 5 );
|
|
timeoutObj endon( "watchGrenadeTowardsPlayerTimeout" );
|
|
|
|
type = self.grenadeWeapon;
|
|
|
|
grenade = self getGrenadeIThrew();
|
|
if ( !isdefined( grenade ) )
|
|
{
|
|
// the throw failed. maybe we died. =(
|
|
return;
|
|
}
|
|
|
|
setGrenadeTimer( activeGrenadeTimer, min( gettime() + 5000, nextGrenadeTimeToUse ) );
|
|
|
|
/#
|
|
grenade thread grenadeDebug( "Incoming", 5 );
|
|
#/
|
|
|
|
goodRadiusSqrd = 250 * 250;
|
|
giveUpRadiusSqrd = 400 * 400;
|
|
if ( type == "flash_grenade" )
|
|
{
|
|
goodRadiusSqrd = 900 * 900;
|
|
giveUpRadiusSqrd = 1300 * 1300;
|
|
}
|
|
|
|
playersToCheck = level.players;
|
|
|
|
// wait for grenade to settle
|
|
prevorigin = grenade.origin;
|
|
while ( 1 )
|
|
{
|
|
wait .1;
|
|
|
|
if ( !isdefined( grenade ) )
|
|
break;
|
|
|
|
if ( distanceSquared( grenade.origin, prevorigin ) < 400 ) // sqr(20)
|
|
{
|
|
/#
|
|
grenade thread grenadeDebug( "Landed", 5 );
|
|
#/
|
|
// grenade is stationary. check if it's near any players
|
|
newPlayersToCheck = [];
|
|
for ( i = 0; i < playersToCheck.size; i++ )
|
|
{
|
|
player = playersToCheck[ i ];
|
|
distSqrd = distanceSquared( grenade.origin, player.origin );
|
|
if ( distSqrd < goodRadiusSqrd )
|
|
{
|
|
/#
|
|
grenade thread grenadeDebug( "Landed near player", 5 );
|
|
#/
|
|
|
|
player grenadeLandedNearPlayer( activeGrenadeTimer, nextGrenadeTimeToUse );
|
|
}
|
|
else if ( distSqrd < giveUpRadiusSqrd )
|
|
{
|
|
newPlayersToCheck[ newPlayersToCheck.size ] = player;
|
|
}
|
|
}
|
|
playersToCheck = newPlayersToCheck;
|
|
if ( playersToCheck.size == 0 )
|
|
break;
|
|
}
|
|
prevorigin = grenade.origin;
|
|
}
|
|
}
|
|
|
|
grenadeLandedNearPlayer( activeGrenadeTimer, nextGrenadeTimeToUse )
|
|
{
|
|
player = self;
|
|
|
|
// the grenade landed near the player! =D
|
|
anim.throwGrenadeAtPlayerASAP = undefined;
|
|
|
|
if ( gettime() - player.lastGrenadeLandedNearPlayerTime < 3000 )
|
|
{
|
|
// double grenade happened
|
|
player.grenadeTimers[ "double_grenade" ] = gettime() + player.gs.playerDoubleGrenadeTime;
|
|
}
|
|
|
|
player.lastGrenadeLandedNearPlayerTime = gettime();
|
|
|
|
oldValue = player.grenadeTimers[ activeGrenadeTimer.timerName ];
|
|
player.grenadeTimers[ activeGrenadeTimer.timerName ] = max( nextGrenadeTimeToUse, oldValue );
|
|
}
|
|
|
|
getGrenadeIThrew()
|
|
{
|
|
self endon( "killanimscript" );
|
|
self waittill( "grenade_fire", grenade );
|
|
return grenade;
|
|
}
|
|
|
|
watchGrenadeTowardsPlayerTimeout( timerlength )
|
|
{
|
|
wait timerlength;
|
|
self notify( "watchGrenadeTowardsPlayerTimeout" );
|
|
}
|
|
|
|
|
|
attachGrenadeModel( model, tag )
|
|
{
|
|
self attach( model, tag );
|
|
thread detachGrenadeOnScriptChange( model, tag );
|
|
return tag;
|
|
}
|
|
|
|
|
|
detachGrenadeOnScriptChange( model, tag )
|
|
{
|
|
//self endon ("death"); // don't end on death or it will hover when we die!
|
|
self endon( "stop grenade check" );
|
|
self waittill( "killanimscript" );
|
|
|
|
if ( !isdefined( self ) )// we may be dead but still defined. if we're not defined, we were probably deleted.
|
|
return;
|
|
|
|
if ( isdefined( self.oldGrenAwareness ) )
|
|
{
|
|
self.grenadeawareness = self.oldGrenAwareness;
|
|
self.oldGrenAwareness = undefined;
|
|
}
|
|
|
|
self detach( model, tag );
|
|
}
|
|
|
|
offsetToOrigin( start )
|
|
{
|
|
forward = anglestoforward( self.angles );
|
|
right = anglestoright( self.angles );
|
|
up = anglestoup( self.angles );
|
|
forward = vector_multiply( forward, start[ 0 ] );
|
|
right = vector_multiply( right, start[ 1 ] );
|
|
up = vector_multiply( up, start[ 2 ] );
|
|
return( forward + right + up );
|
|
}
|
|
|
|
grenadeLine( start, end )
|
|
{
|
|
level notify( "armoffset" );
|
|
level endon( "armoffset" );
|
|
|
|
start = self.origin + offsetToOrigin( start );
|
|
for ( ;; )
|
|
{
|
|
line( start, end, ( 1, 0, 1 ) );
|
|
print3d( start, start, ( 0.2, 0.5, 1.0 ), 1, 1 ); // origin, text, RGB, alpha, scale
|
|
print3d( end, end, ( 0.2, 0.5, 1.0 ), 1, 1 ); // origin, text, RGB, alpha, scale
|
|
wait( 0.05 );
|
|
}
|
|
}
|
|
|
|
getGrenadeDropVelocity()
|
|
{
|
|
yaw = randomFloat( 360 );
|
|
pitch = randomFloatRange( 30, 75 );
|
|
|
|
amntz = sin( pitch );
|
|
cospitch = cos( pitch );
|
|
|
|
amntx = cos( yaw ) * cospitch;
|
|
amnty = sin( yaw ) * cospitch;
|
|
|
|
speed = randomFloatRange( 100, 200 );
|
|
|
|
velocity = ( amntx, amnty, amntz ) * speed;
|
|
return velocity;
|
|
}
|
|
|
|
dropGrenade()
|
|
{
|
|
grenadeOrigin = self GetTagOrigin( "tag_inhand" );
|
|
velocity = getGrenadeDropVelocity();
|
|
self MagicGrenadeManual( grenadeOrigin, velocity, 3 );
|
|
}
|
|
|
|
lookForBetterCover()
|
|
{
|
|
// don't do cover searches if we don't have an enemy.
|
|
if ( !isdefined( self.enemy ) )
|
|
return false;
|
|
|
|
if ( self.fixedNode || self.doingAmbush )
|
|
return false;
|
|
|
|
//prof_begin( "lookForBetterCover" );
|
|
|
|
node = self getBestCoverNodeIfAvailable();
|
|
|
|
if ( isdefined( node ) )
|
|
{
|
|
//prof_end( "lookForBetterCover" );
|
|
return useCoverNodeIfPossible( node );
|
|
}
|
|
|
|
//prof_end( "lookForBetterCover" );
|
|
return false;
|
|
}
|
|
|
|
getBestCoverNodeIfAvailable()
|
|
{
|
|
//prof_begin( "getBestCoverNodeIfAvailable" );
|
|
node = self FindBestCoverNode();
|
|
|
|
if ( !isdefined( node ) )
|
|
{
|
|
//prof_end( "getBestCoverNodeIfAvailable" );
|
|
return undefined;
|
|
}
|
|
|
|
currentNode = self GetClaimedNode();
|
|
if ( isdefined( currentNode ) && node == currentNode )
|
|
{
|
|
//prof_end( "getBestCoverNodeIfAvailable" );
|
|
return undefined;
|
|
}
|
|
|
|
// work around FindBestCoverNode() resetting my .node in rare cases involving overlapping nodes
|
|
// This prevents us from thinking we've found a new node somewhere when in reality it's the one we're already at, so we won't abort our script.
|
|
if ( isdefined( self.coverNode ) && node == self.coverNode )
|
|
{
|
|
//prof_end( "getBestCoverNodeIfAvailable" );
|
|
return undefined;
|
|
}
|
|
|
|
//prof_end( "getBestCoverNodeIfAvailable" );
|
|
return node;
|
|
}
|
|
|
|
useCoverNodeIfPossible( node )
|
|
{
|
|
oldKeepNodeInGoal = self.keepClaimedNodeIfValid;
|
|
oldKeepNode = self.keepClaimedNode;
|
|
self.keepClaimedNodeIfValid = false;
|
|
self.keepClaimedNode = false;
|
|
|
|
if ( self UseCoverNode( node ) )
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
/#self thread DebugFailedCoverUsage( node );#/
|
|
}
|
|
|
|
self.keepClaimedNodeIfValid = oldKeepNodeInGoal;
|
|
self.keepClaimedNode = oldKeepNode;
|
|
|
|
return false;
|
|
}
|
|
|
|
/#
|
|
DebugFailedCoverUsage( node )
|
|
{
|
|
if ( getdvar( "scr_debugfailedcover" ) == "" )
|
|
setdvar( "scr_debugfailedcover", "0" );
|
|
if ( getdebugdvarint( "scr_debugfailedcover" ) == 1 )
|
|
{
|
|
self endon( "death" );
|
|
for ( i = 0; i < 20; i++ )
|
|
{
|
|
line( self.origin, node.origin );
|
|
print3d( node.origin, "failed" );
|
|
wait .05;
|
|
}
|
|
}
|
|
}
|
|
#/
|
|
|
|
// this function seems okish,
|
|
// but the idea behind FindReacquireNode() is that you call it once,
|
|
// and then call GetReacquireNode() many times until it returns undefined.
|
|
// if we're just taking the first node (the best), we might as well just be using
|
|
// FindBestCoverNode().
|
|
/*
|
|
tryReacquireNode()
|
|
{
|
|
self FindReacquireNode();
|
|
node = self GetReacquireNode();
|
|
if (!isdefined(node))
|
|
return false;
|
|
return (self UseReacquireNode(node));
|
|
}
|
|
*/
|
|
|
|
shouldHelpAdvancingTeammate()
|
|
{
|
|
// if teammate advanced recently
|
|
if ( level.advanceToEnemyGroup[ self.team ] > 0 && level.advanceToEnemyGroup[ self.team ] < level.advanceToEnemyGroupMax )
|
|
{
|
|
if ( gettime() - level.lastAdvanceToEnemyTime[ self.team ] > 4000 )
|
|
return false;
|
|
|
|
leadAttacker = level.lastAdvanceToEnemyAttacker[ self.team ];
|
|
nearLeadAttacker = isdefined( leadAttacker ) && distanceSquared( self.origin, leadAttacker.origin ) < 256 * 256;
|
|
|
|
if ( ( nearLeadAttacker || distanceSquared( self.origin, level.lastAdvanceToEnemySrc[ self.team ] ) < 256 * 256 ) &&
|
|
( !isdefined( self.enemy ) || distanceSquared( self.enemy.origin, level.lastAdvanceToEnemyDest[ self.team ] ) < 512 * 512 ) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
checkAdvanceOnEnemyConditions()
|
|
{
|
|
if ( !isdefined( level.lastAdvanceToEnemyTime[ self.team ] ) )
|
|
return false;
|
|
|
|
if ( shouldHelpAdvancingTeammate() )
|
|
return true;
|
|
|
|
if ( gettime() - level.lastAdvanceToEnemyTime[ self.team ] < level.advanceToEnemyInterval )
|
|
return false;
|
|
|
|
|
|
if ( !isSentient( self.enemy ) )
|
|
return false;
|
|
|
|
if ( level.advanceToEnemyGroup[ self.team ] )
|
|
level.advanceToEnemyGroup[ self.team ] = 0;
|
|
|
|
if ( getAICount( self.team ) < getAICount( self.enemy.team ) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
tryRunningToEnemy( ignoreSuppression )
|
|
{
|
|
if ( !isdefined( self.enemy ) )
|
|
return false;
|
|
|
|
if ( self.fixedNode )
|
|
return false;
|
|
|
|
if ( self.combatMode == "ambush" || self.combatMode == "ambush_nodes_only" )
|
|
return false;
|
|
|
|
if ( !self isingoal( self.enemy.origin ) )
|
|
return false;
|
|
|
|
if ( self isLongRangeAI() )
|
|
return false;
|
|
|
|
if ( !checkAdvanceOnEnemyConditions() )
|
|
return false;
|
|
|
|
self FindReacquireDirectPath( ignoreSuppression );
|
|
|
|
// TrimPathToAttack is supposed to be called multiple times, until it returns false.
|
|
// it trims the path a little more each time, until trimming it more would make the enemy invisible from the end of the path.
|
|
// we're skipping this step and just running until we get within close range of the enemy.
|
|
// maybe later we can periodically check while moving if the enemy is visible, and if so, enter exposed.
|
|
//self TrimPathToAttack();
|
|
|
|
if ( self ReacquireMove() )
|
|
{
|
|
self.keepClaimedNodeIfValid = false;
|
|
self.keepClaimedNode = false;
|
|
|
|
self.a.magicReloadWhenReachEnemy = true;
|
|
|
|
if ( level.advanceToEnemyGroup[ self.team ] == 0 )
|
|
{
|
|
level.lastAdvanceToEnemyTime[ self.team ] = gettime();
|
|
level.lastAdvanceToEnemyAttacker[ self.team ] = self;
|
|
}
|
|
|
|
level.lastAdvanceToEnemySrc[ self.team ] = self.origin;
|
|
level.lastAdvanceToEnemyDest[ self.team ] = self.enemy.origin;
|
|
|
|
level.advanceToEnemyGroup[ self.team ]++;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
delayedBadplace( org )
|
|
{
|
|
self endon( "death" );
|
|
wait( 0.5 );
|
|
/#
|
|
if ( getdebugdvar( "debug_displace" ) == "on" )
|
|
thread badplacer( 5, org, 16 );
|
|
#/
|
|
|
|
string = "" + anim.badPlaceInt;
|
|
badplace_cylinder( string, 5, org, 16, 64, self.team );
|
|
anim.badPlaces[ anim.badPlaces.size ] = string;
|
|
if ( anim.badPlaces.size >= 10 )// too many badplaces, delete the oldest one and then remove it from the array
|
|
{
|
|
newArray = [];
|
|
for ( i = 1;i < anim.badPlaces.size;i++ )
|
|
newArray[ newArray.size ] = anim.badPlaces[ i ];
|
|
badplace_delete( anim.badPlaces[ 0 ] );
|
|
anim.badPlaces = newArray;
|
|
}
|
|
anim.badPlaceInt++ ;
|
|
if ( anim.badPlaceInt > 10 )
|
|
anim.badPlaceInt -= 20;
|
|
}
|
|
|
|
valueIsWithin( value, min, max )
|
|
{
|
|
if ( value > min && value < max )
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
getGunYawToShootEntOrPos()
|
|
{
|
|
if ( !isdefined( self.shootPos ) )
|
|
{
|
|
assert( !isdefined( self.shootEnt ) );
|
|
return 0;
|
|
}
|
|
|
|
yaw = self getMuzzleAngle()[ 1 ] - GetYaw( self.shootPos );
|
|
yaw = AngleClamp180( yaw );
|
|
return yaw;
|
|
}
|
|
|
|
getGunPitchToShootEntOrPos()
|
|
{
|
|
if ( !isdefined( self.shootPos ) )
|
|
{
|
|
assert( !isdefined( self.shootEnt ) );
|
|
return 0;
|
|
}
|
|
|
|
pitch = self getMuzzleAngle()[ 0 ] - VectorToAngles( self.shootPos - self getMuzzlePos() )[ 0 ];
|
|
pitch = AngleClamp180( pitch );
|
|
return pitch;
|
|
}
|
|
|
|
getPitchToEnemy()
|
|
{
|
|
if ( !isdefined( self.enemy ) )
|
|
return 0;
|
|
|
|
vectorToEnemy = self.enemy getshootatpos() - self getshootatpos();
|
|
vectorToEnemy = vectornormalize( vectortoenemy );
|
|
pitchDelta = 360 - vectortoangles( vectorToEnemy )[ 0 ];
|
|
|
|
return AngleClamp180( pitchDelta );
|
|
}
|
|
|
|
getPitchToSpot( spot )
|
|
{
|
|
if ( !isdefined( spot ) )
|
|
return 0;
|
|
|
|
vectorToEnemy = spot - self getshootatpos();
|
|
vectorToEnemy = vectornormalize( vectortoenemy );
|
|
pitchDelta = 360 - vectortoangles( vectorToEnemy )[ 0 ];
|
|
|
|
return AngleClamp180( pitchDelta );
|
|
}
|
|
|
|
watchReloading()
|
|
{
|
|
// this only works on the player.
|
|
self.isreloading = false;
|
|
self.lastReloadStartTime = -1;
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "reload_start" );
|
|
self.isreloading = true;
|
|
self.lastReloadStartTime = GetTime();
|
|
|
|
self waittillreloadfinished();
|
|
self.isreloading = false;
|
|
}
|
|
}
|
|
|
|
waittillReloadFinished()
|
|
{
|
|
self thread timedNotify( 4, "reloadtimeout" );
|
|
self endon( "reloadtimeout" );
|
|
self endon( "weapon_taken" );
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "reload" );
|
|
|
|
weap = self getCurrentWeapon();
|
|
if ( weap == "none" )
|
|
break;
|
|
|
|
if ( self getCurrentWeaponClipAmmo() >= weaponClipSize( weap ) )
|
|
break;
|
|
}
|
|
self notify( "reloadtimeout" );
|
|
}
|
|
|
|
timedNotify( time, msg )
|
|
{
|
|
self endon( msg );
|
|
wait time;
|
|
self notify( msg );
|
|
}
|
|
|
|
maxFlashThrowDistSq = 768 * 768;
|
|
minGrenadeThrowDistSq = 200 * 200;
|
|
maxGrenadeThrowDistSq = 1250 * 1250;
|
|
|
|
checkGrenadeThrowDist()
|
|
{
|
|
diff = self.enemy.origin - self.origin;
|
|
distSq = lengthSquared( ( diff[ 0 ], diff[ 1 ], 0 ) );
|
|
|
|
// Flashbangs are threated separately
|
|
if ( self.grenadeWeapon == "flash_grenade" )
|
|
return (distSq < maxFlashThrowDistSq);
|
|
|
|
// All other grenades have a min/max range
|
|
return (distSq >= minGrenadeThrowDistSq) && (distSq <= maxGrenadeThrowDistSq);
|
|
}
|
|
|
|
monitorFlash()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "stop_monitoring_flash" );
|
|
|
|
while ( 1 )
|
|
{
|
|
// "flashbang" is code notifying that the AI can be flash banged
|
|
// "doFlashBanged" is sent below if the AI should do flash banged behavior
|
|
self waittill( "flashbang", origin, amount_distance, amount_angle, attacker, attackerteam );
|
|
|
|
if ( isDefined( self.flashBangImmunity ) && self.flashBangImmunity )
|
|
continue;
|
|
|
|
if ( isdefined( self.script_immunetoflash ) && self.script_immunetoflash != 0 )
|
|
continue;
|
|
|
|
if ( isdefined( self.onSnowMobile ) )
|
|
continue;
|
|
|
|
if ( isdefined( self.team ) && isdefined( attackerteam ) && self.team == attackerteam )
|
|
{
|
|
// AI get a break when their own team flashbangs them.
|
|
amount_distance = 3 * ( amount_distance - .75 );
|
|
if ( amount_distance < 0 )
|
|
continue;
|
|
|
|
if ( isdefined( self.teamFlashbangImmunity ) )
|
|
continue;
|
|
}
|
|
|
|
// at 200 or less of the full range of 1000 units, get the full effect
|
|
minamountdist = 0.2;
|
|
if ( amount_distance > 1 - minamountdist )
|
|
amount_distance = 1.0;
|
|
else
|
|
amount_distance = amount_distance / ( 1 - minamountdist );
|
|
|
|
duration = 4.5 * amount_distance;
|
|
|
|
if ( duration < 0.25 )
|
|
continue;
|
|
|
|
self.flashingTeam = attackerteam;
|
|
self flashBangStart( duration );
|
|
self notify( "doFlashBanged", origin, attacker );
|
|
}
|
|
}
|
|
|
|
isShotgunAI()
|
|
{
|
|
return isShotgun( self.primaryweapon );
|
|
}
|
|
|
|
isSniper()
|
|
{
|
|
return isSniperRifle( self.primaryweapon );
|
|
}
|
|
|
|
isLongRangeAI()
|
|
{
|
|
return isSniper() || usingRocketLauncher();
|
|
}
|
|
|
|
fasterAnimSpeed()
|
|
{
|
|
return 1.5;
|
|
}
|
|
|
|
randomfasterAnimSpeed()
|
|
{
|
|
return randomfloatrange( 1, 1.2 );
|
|
}
|
|
|
|
getRandomCoverMode( modes )
|
|
{
|
|
if ( modes.size == 0 )
|
|
return undefined;
|
|
if ( modes.size == 1 )
|
|
return modes[0];
|
|
|
|
// 20% chance of attempting to repeat same corner mode
|
|
if ( isdefined( self.a.prevAttack ) && randomint( 100 ) > 20 )
|
|
{
|
|
foreach ( i, mode in modes )
|
|
{
|
|
if ( mode == self.a.prevAttack )
|
|
{
|
|
if ( i < modes.size - 1 )
|
|
modes[ i ] = modes[ modes.size - 1 ];
|
|
|
|
modes[ modes.size - 1 ] = undefined;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return modes[ randomint( modes.size ) ];
|
|
}
|
|
|
|
|
|
player_sees_my_scope()
|
|
{
|
|
// player sees the scope glint if the dot is within a certain range
|
|
start = self geteye();
|
|
foreach ( player in level.players )
|
|
{
|
|
if ( !self cansee( player ) )
|
|
continue;
|
|
|
|
end = player GetEye();
|
|
|
|
angles = VectorToAngles( start - end );
|
|
forward = AnglesToForward( angles );
|
|
player_angles = player GetPlayerAngles();
|
|
player_forward = AnglesToForward( player_angles );
|
|
|
|
dot = VectorDot( forward, player_forward );
|
|
if ( dot < 0.805 )
|
|
continue;
|
|
|
|
if ( cointoss() && dot >= 0.996 )
|
|
continue;
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
} |