IW4-Dump-Files/animscripts/combat_utility.gsc

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;
}