972 lines
23 KiB
Plaintext
972 lines
23 KiB
Plaintext
#include maps\_utility;
|
|
#include animscripts\combat_utility;
|
|
#include animscripts\utility;
|
|
#include animscripts\shared;
|
|
#include common_scripts\utility;
|
|
|
|
/*
|
|
This file contains the overall behavior for all "whack-a-mole" cover nodes.
|
|
|
|
Callbacks which must be defined:
|
|
|
|
All callbacks should return true or false depending on whether they succeeded in doing something.
|
|
If functionality for a callback isn't available, just don't define it.
|
|
|
|
mainLoopStart()
|
|
optional
|
|
reload()
|
|
plays a reload animation in a hidden position
|
|
leaveCoverAndShoot()
|
|
does the main attacking; steps out or stands up and fires, goes back to hiding.
|
|
should obey orders from decideWhatAndHowToShoot in shoot_behavior.gsc.
|
|
look( maxtime )
|
|
looks for up to maxtime, stopping and returning if enemy becomes visible or if suppressed
|
|
fastlook()
|
|
looks quickly
|
|
idle()
|
|
idles until the "end_idle" notify.
|
|
flinch()
|
|
flinches briefly (1-2 seconds), doesn't need to return true or false.
|
|
grenade( throwAt )
|
|
steps out and throws a grenade at the given player / ai
|
|
grenadehidden( throwAt )
|
|
throws a grenade at the given player / ai without leaving cover
|
|
blindfire()
|
|
blindfires from cover
|
|
|
|
example:
|
|
behaviorCallbacks = spawnstruct();
|
|
behaviorCallbacks.reload = ::reload;
|
|
...
|
|
animscripts\cover_behavior::main( behaviorCallbacks );
|
|
|
|
*/
|
|
|
|
#using_animtree( "generic_human" );
|
|
|
|
|
|
MELEE_GRACE_PERIOD_REQUIRED_TIME = 3000;
|
|
MELEE_GRACE_PERIOD_GIVEN_TIME = 5000;
|
|
|
|
main( behaviorCallbacks )
|
|
{
|
|
self.couldntSeeEnemyPos = self.origin;// ( set couldntSeeEnemyPos to a place the enemy can't be while we're in corner behavior )
|
|
|
|
behaviorStartTime = gettime();
|
|
coverTimers = spawnstruct();
|
|
coverTimers.nextAllowedLookTime = behaviorStartTime - 1;
|
|
coverTimers.nextAllowedSuppressTime = behaviorStartTime - 1;
|
|
|
|
// we won't look for better cover purely out of boredom until this time
|
|
resetLookForBetterCoverTime();
|
|
resetRespondToDeathTime();
|
|
|
|
self.seekOutEnemyTime = gettime();
|
|
|
|
self.a.lastEncounterTime = behaviorStartTime;
|
|
self.a.idlingAtCover = false;
|
|
self.a.movement = "stop";
|
|
|
|
// if we break out of cover mode after this time, we will get a grace period during which we can melee charge the player
|
|
self.meleeCoverChargeMinTime = behaviorStartTime + MELEE_GRACE_PERIOD_REQUIRED_TIME;
|
|
|
|
/#
|
|
if ( getdvar( "scr_coveridle" ) == "1" )
|
|
self.coverNode.script_onlyidle = true;
|
|
#/
|
|
|
|
self thread watchSuppression();
|
|
|
|
desynched = ( gettime() > 2500 );
|
|
|
|
correctAngles = getCorrectCoverAngles();
|
|
|
|
for ( ;; )
|
|
{
|
|
if ( shouldHelpAdvancingTeammate() )
|
|
{
|
|
if ( tryRunningToEnemy( true ) )
|
|
{
|
|
wait 0.05;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( isdefined( behaviorCallbacks.mainLoopStart ) )
|
|
{
|
|
startTime = gettime();
|
|
self thread endIdleAtFrameEnd();
|
|
|
|
[[ behaviorCallbacks.mainLoopStart ]]();
|
|
|
|
if ( gettime() == startTime )
|
|
self notify( "dont_end_idle" );
|
|
}
|
|
|
|
if ( isdefined( behaviorCallbacks.moveToNearByCover ) )
|
|
{
|
|
if ( [[ behaviorCallbacks.moveToNearByCover ]]() )
|
|
continue;
|
|
}
|
|
|
|
self safeTeleport( self.covernode.origin, correctAngles );
|
|
|
|
if ( !desynched )
|
|
{
|
|
idle( behaviorCallbacks, 0.05 + randomfloat( 1.5 ) );
|
|
desynched = true;
|
|
continue;
|
|
}
|
|
|
|
if ( doNonAttackCoverBehavior( behaviorCallbacks ) )
|
|
continue;
|
|
|
|
if ( isdefined( anim.throwGrenadeAtPlayerASAP ) && isAlive( level.player ) )
|
|
{
|
|
if ( tryThrowingGrenade( behaviorCallbacks, level.player ) )
|
|
continue;
|
|
}
|
|
|
|
if ( respondToDeadTeammate() )
|
|
return;
|
|
|
|
// determine visibility and suppressability of enemy.
|
|
visibleEnemy = false;
|
|
suppressableEnemy = false;
|
|
if ( isalive( self.enemy ) )
|
|
{
|
|
visibleEnemy = isEnemyVisibleFromExposed();
|
|
suppressableEnemy = canSuppressEnemyFromExposed();
|
|
}
|
|
|
|
// decide what to do.
|
|
if ( visibleEnemy )
|
|
{
|
|
if ( self.a.getBoredOfThisNodeTime < gettime() )
|
|
{
|
|
if ( lookForBetterCover() )
|
|
return;
|
|
}
|
|
|
|
attackVisibleEnemy( behaviorCallbacks );
|
|
}
|
|
else
|
|
{
|
|
if ( isdefined( self.aggressiveMode ) || enemyIsHiding() )
|
|
{
|
|
if ( advanceOnHidingEnemy() )
|
|
return;
|
|
}
|
|
|
|
if ( suppressableEnemy )
|
|
{
|
|
attackSuppressableEnemy( behaviorCallbacks, coverTimers );
|
|
}
|
|
else
|
|
{
|
|
if ( attackNothingToDo( behaviorCallbacks, coverTimers ) )
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
end_script( coverMode )
|
|
{
|
|
self.turnToMatchNode = undefined;
|
|
self.a.prevAttack = undefined;
|
|
|
|
if ( isDefined( self.meleeCoverChargeMinTime ) && (self.meleeCoverChargeMinTime <= getTime()) )
|
|
{
|
|
// give the AI a chance to charge the player if he forced him out of cover
|
|
self.meleeCoverChargeGraceEndTime = getTime() + MELEE_GRACE_PERIOD_GIVEN_TIME;
|
|
self.meleeCoverChargeMinTime = undefined;
|
|
}
|
|
}
|
|
|
|
|
|
getCorrectCoverAngles()
|
|
{
|
|
correctAngles = ( self.coverNode.angles[ 0 ], getNodeForwardYaw( self.coverNode ), self.coverNode.angles[ 2 ] );
|
|
return correctAngles;
|
|
}
|
|
|
|
|
|
RESPOND_TO_DEATH_RETRY_INTERVAL = 30 * 1000;
|
|
|
|
respondToDeadTeammate()
|
|
{
|
|
if ( self atDangerousNode() && self.a.respondToDeathTime < gettime() )
|
|
{
|
|
if ( lookForBetterCover() )
|
|
return true;
|
|
|
|
self.a.respondToDeathTime = gettime() + RESPOND_TO_DEATH_RETRY_INTERVAL;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
doNonAttackCoverBehavior( behaviorCallbacks )
|
|
{
|
|
/#
|
|
if ( isDefined( self.coverNode.script_onlyidle ) )
|
|
{
|
|
assert( self.coverNode.script_onlyidle );// true or undefined
|
|
idle( behaviorCallbacks );
|
|
return true;
|
|
}
|
|
#/
|
|
|
|
// if we're suppressed, we do other things.
|
|
if ( suppressedBehavior( behaviorCallbacks ) )
|
|
{
|
|
if ( isEnemyVisibleFromExposed() )
|
|
resetSeekOutEnemyTime();
|
|
self.a.lastEncounterTime = gettime();
|
|
return true;
|
|
}
|
|
|
|
// reload if we need to; everything in this loop involves shooting.
|
|
if ( coverReload( behaviorCallbacks, 0 ) )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
attackVisibleEnemy( behaviorCallbacks )
|
|
{
|
|
if ( distanceSquared( self.origin, self.enemy.origin ) > 750 * 750 )
|
|
{
|
|
if ( tryThrowingGrenade( behaviorCallbacks, self.enemy ) )
|
|
return;
|
|
}
|
|
|
|
if ( leaveCoverAndShoot( behaviorCallbacks, "normal" ) )
|
|
{
|
|
resetSeekOutEnemyTime();
|
|
self.a.lastEncounterTime = gettime();
|
|
}
|
|
else
|
|
{
|
|
idle( behaviorCallbacks );
|
|
}
|
|
}
|
|
|
|
attackSuppressableEnemy( behaviorCallbacks, coverTimers )
|
|
{
|
|
if ( self.doingAmbush )
|
|
{
|
|
if ( leaveCoverAndShoot( behaviorCallbacks, "ambush" ) )
|
|
return;
|
|
}
|
|
else if ( self.provideCoveringFire || gettime() >= coverTimers.nextAllowedSuppressTime )
|
|
{
|
|
preferredActivity = "suppress";
|
|
if ( !self.provideCoveringFire && ( gettime() - self.lastSuppressionTime ) > 5000 && randomint( 3 ) < 2 )
|
|
preferredActivity = "ambush";
|
|
else if ( !self animscripts\shoot_behavior::shouldSuppress() )
|
|
preferredActivity = "ambush";
|
|
|
|
if ( leaveCoverAndShoot( behaviorCallbacks, preferredActivity ) )
|
|
{
|
|
coverTimers.nextAllowedSuppressTime = gettime() + randomintrange( 3000, 20000 );
|
|
// if they're there, we've seen them
|
|
if ( isEnemyVisibleFromExposed() )
|
|
self.a.lastEncounterTime = gettime();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( tryThrowingGrenade( behaviorCallbacks, self.enemy ) )
|
|
return;
|
|
|
|
idle( behaviorCallbacks );
|
|
}
|
|
|
|
|
|
attackNothingToDo( behaviorCallbacks, coverTimers )
|
|
{
|
|
if ( coverReload( behaviorCallbacks, 0.1 ) )
|
|
return false;
|
|
|
|
if ( isdefined( self.enemy ) )
|
|
{
|
|
if ( tryThrowingGrenade( behaviorCallbacks, self.enemy ) )
|
|
return false;
|
|
}
|
|
|
|
if ( !self.doingAmbush && gettime() >= coverTimers.nextAllowedLookTime )
|
|
{
|
|
if ( lookForEnemy( behaviorCallbacks ) )
|
|
{
|
|
coverTimers.nextAllowedLookTime = gettime() + randomintrange( 4000, 15000 );
|
|
|
|
// if they're there, we've seen them
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// we're *really* bored right now
|
|
if ( gettime() > self.a.getBoredOfThisNodeTime )
|
|
{
|
|
if ( cantFindAnythingToDo() )
|
|
return true;
|
|
}
|
|
|
|
if ( self.doingAmbush || ( gettime() >= coverTimers.nextAllowedSuppressTime && isdefined( self.enemy ) ) )
|
|
{
|
|
// be ready to ambush them if they happen to show up
|
|
if ( leaveCoverAndShoot( behaviorCallbacks, "ambush" ) )
|
|
{
|
|
if ( isEnemyVisibleFromExposed() )
|
|
resetSeekOutEnemyTime();
|
|
self.a.lastEncounterTime = gettime();
|
|
coverTimers.nextAllowedSuppressTime = gettime() + randomintrange( 6000, 20000 );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
idle( behaviorCallbacks );
|
|
return false;
|
|
}
|
|
|
|
|
|
isEnemyVisibleFromExposed()
|
|
{
|
|
if ( !isdefined( self.enemy ) )
|
|
return false;
|
|
|
|
// if we couldn't see our enemy last time we stepped out, and they haven't moved, assume we still can't see them.
|
|
if ( distanceSquared( self.enemy.origin, self.couldntSeeEnemyPos ) < 16 * 16 )
|
|
return false;
|
|
else
|
|
return canSeeEnemyFromExposed();
|
|
}
|
|
|
|
suppressedBehavior( behaviorCallbacks )
|
|
{
|
|
if ( !isSuppressedWrapper() )
|
|
return false;
|
|
|
|
nextAllowedBlindfireTime = gettime();
|
|
|
|
justlooked = true;
|
|
|
|
//prof_begin( "suppressedBehavior" );
|
|
|
|
while ( isSuppressedWrapper() )
|
|
{
|
|
justlooked = false;
|
|
|
|
self safeTeleport( self.coverNode.origin );
|
|
|
|
tryMovingNodes = true;
|
|
|
|
// guys that favor blindfire should try to blindfire instead of move a lot more
|
|
if ( isdefined( self.favor_blindfire ) )
|
|
tryMovingNodes = coinToss();
|
|
|
|
if ( tryMovingNodes )
|
|
{
|
|
if ( tryToGetOutOfDangerousSituation( behaviorCallbacks ) )
|
|
{
|
|
self notify( "killanimscript" );
|
|
//prof_end( "suppressedBehavior" );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// if we're only at a concealment node, and it's not providing cover, we shouldn't try to use the cover to keep us safe!
|
|
if ( self.a.atConcealmentNode && self canSeeEnemy() )
|
|
{
|
|
//prof_end( "suppressedBehavior" );
|
|
return false;
|
|
}
|
|
|
|
if ( isEnemyVisibleFromExposed() || canSuppressEnemyFromExposed() )
|
|
{
|
|
if ( isdefined( anim.throwGrenadeAtPlayerASAP ) && isAlive( level.player ) )
|
|
{
|
|
if ( tryThrowingGrenade( behaviorCallbacks, level.player ) )
|
|
continue;
|
|
}
|
|
|
|
if ( coverReload( behaviorCallbacks, 0 ) )
|
|
continue;
|
|
|
|
if ( self.team != "allies" && gettime() >= nextAllowedBlindfireTime )
|
|
{
|
|
if ( blindfire( behaviorCallbacks ) )
|
|
{
|
|
nextAllowedBlindfireTime = gettime();
|
|
if ( !isdefined( self.favor_blindfire ) )
|
|
nextAllowedBlindfireTime += randomintrange( 3000, 12000 );
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( tryThrowingGrenade( behaviorCallbacks, self.enemy ) )
|
|
{
|
|
justlooked = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( coverReload( behaviorCallbacks, 0.1 ) )
|
|
continue;
|
|
|
|
//prof_end( "suppressedBehavior" );
|
|
idle( behaviorCallbacks );
|
|
}
|
|
|
|
if ( !justlooked && randomint( 2 ) == 0 )
|
|
lookfast( behaviorCallbacks );
|
|
|
|
//prof_end( "suppressedBehavior" );
|
|
return true;
|
|
}
|
|
|
|
// returns array of integers 0 through n-1, in random order
|
|
getPermutation( n )
|
|
{
|
|
permutation = [];
|
|
assert( n > 0 );
|
|
if ( n == 1 )
|
|
{
|
|
permutation[ 0 ] = 0;
|
|
}
|
|
else if ( n == 2 )
|
|
{
|
|
permutation[ 0 ] = randomint( 2 );
|
|
permutation[ 1 ] = 1 - permutation[ 0 ];
|
|
}
|
|
else
|
|
{
|
|
for ( i = 0; i < n; i++ )
|
|
permutation[ i ] = i;
|
|
for ( i = 0; i < n; i++ )
|
|
{
|
|
switchIndex = i + randomint( n - i );
|
|
temp = permutation[ switchIndex ];
|
|
permutation[ SwitchIndex ] = permutation[ i ];
|
|
permutation[ i ] = temp;
|
|
}
|
|
}
|
|
return permutation;
|
|
}
|
|
|
|
callOptionalBehaviorCallback( callback, arg, arg2, arg3 )
|
|
{
|
|
if ( !isdefined( callback ) )
|
|
return false;
|
|
|
|
//prof_begin( "callOptionalBehaviorCallback" );
|
|
self thread endIdleAtFrameEnd();
|
|
|
|
starttime = gettime();
|
|
|
|
val = undefined;
|
|
if ( isdefined( arg3 ) )
|
|
val = [[ callback ]]( arg, arg2, arg3 );
|
|
else if ( isdefined( arg2 ) )
|
|
val = [[ callback ]]( arg, arg2 );
|
|
else if ( isdefined( arg ) )
|
|
val = [[ callback ]]( arg );
|
|
else
|
|
val = [[ callback ]]();
|
|
|
|
/#
|
|
// if this assert fails, a behaviorCallback callback didn't return true or false.
|
|
assert( isdefined( val ) && ( val == true || val == false ) );
|
|
|
|
// behaviorCallbacks must return true if and only if they let time pass.
|
|
// (it is also important that they only let time pass if they did what they were supposed to do,
|
|
// but that's not so easy to enforce.)
|
|
if ( val )
|
|
assert( gettime() != starttime );
|
|
else
|
|
assert( gettime() == starttime );
|
|
#/
|
|
|
|
if ( !val )
|
|
self notify( "dont_end_idle" );
|
|
|
|
//prof_end( "callOptionalBehaviorCallback" );
|
|
|
|
return val;
|
|
}
|
|
|
|
watchSuppression()
|
|
{
|
|
self endon( "killanimscript" );
|
|
|
|
// self.lastSuppressionTime is the last time a bullet whizzed by.
|
|
// self.suppressionStart is the last time we were thinking it was safe when a bullet whizzed by.
|
|
|
|
self.lastSuppressionTime = gettime() - 100000;
|
|
self.suppressionStart = self.lastSuppressionTime;
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "suppression" );
|
|
|
|
time = gettime();
|
|
if ( self.lastSuppressionTime < time - 700 )
|
|
self.suppressionStart = time;
|
|
self.lastSuppressionTime = time;
|
|
}
|
|
}
|
|
|
|
coverReload( behaviorCallbacks, threshold )
|
|
{
|
|
if ( self.bulletsInClip > weaponClipSize( self.weapon ) * threshold )
|
|
return false;
|
|
|
|
self.isreloading = true;
|
|
|
|
result = callOptionalBehaviorCallback( behaviorCallbacks.reload );
|
|
|
|
self.isreloading = false;
|
|
|
|
return result;
|
|
}
|
|
|
|
// initialGoal can be either "normal", "suppress", or "ambush".
|
|
leaveCoverAndShoot( behaviorCallbacks, initialGoal )
|
|
{
|
|
self thread animscripts\shoot_behavior::decideWhatAndHowToShoot( initialGoal );
|
|
|
|
if ( !self.fixedNode && !self.doingAmbush )
|
|
self thread breakOutOfShootingIfWantToMoveUp();
|
|
|
|
val = callOptionalBehaviorCallback( behaviorCallbacks.leaveCoverAndShoot );
|
|
|
|
self notify( "stop_deciding_how_to_shoot" );
|
|
|
|
return val;
|
|
}
|
|
|
|
lookForEnemy( behaviorCallbacks )
|
|
{
|
|
if ( self.a.atConcealmentNode && self canSeeEnemy() )
|
|
return false;
|
|
|
|
if ( self.a.lastEncounterTime + 6000 > gettime() )
|
|
{
|
|
return lookfast( behaviorCallbacks );
|
|
}
|
|
else
|
|
{
|
|
// look slow if possible
|
|
result = callOptionalBehaviorCallback( behaviorCallbacks.look, 2 + randomfloat( 2 ) );
|
|
if ( result )
|
|
return true;
|
|
return callOptionalBehaviorCallback( behaviorCallbacks.fastlook );
|
|
}
|
|
}
|
|
|
|
lookfast( behaviorCallbacks )
|
|
{
|
|
// look fast if possible
|
|
result = callOptionalBehaviorCallback( behaviorCallbacks.fastlook );
|
|
if ( result )
|
|
return true;
|
|
return callOptionalBehaviorCallback( behaviorCallbacks.look, 0 );
|
|
}
|
|
|
|
idle( behaviorCallbacks, howLong )
|
|
{
|
|
self.flinching = false;
|
|
|
|
if ( isdefined( behaviorCallbacks.flinch ) )
|
|
{
|
|
// flinch if we just started getting shot at very recently
|
|
if ( !self.a.idlingAtCover && gettime() - self.suppressionStart < 600 )
|
|
{
|
|
if ( [[ behaviorCallbacks.flinch ]]() )
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// if bullets aren't already whizzing by, idle for now but flinch if we get incoming fire
|
|
self thread flinchWhenSuppressed( behaviorCallbacks );
|
|
}
|
|
}
|
|
|
|
if ( !self.a.idlingAtCover )
|
|
{
|
|
assert( isdefined( behaviorCallbacks.idle ) );// idle must be available!
|
|
self thread idleThread( behaviorCallbacks.idle );// this thread doesn't stop until "end_idle", which must be notified before we start anything else! use endIdleAtFrameEnd() to do this.
|
|
self.a.idlingAtCover = true;
|
|
}
|
|
|
|
if ( isdefined( howLong ) )
|
|
self idleWait( howLong );
|
|
else
|
|
self idleWaitABit();
|
|
|
|
if ( self.flinching )
|
|
self waittill( "flinch_done" );
|
|
|
|
self notify( "stop_waiting_to_flinch" );
|
|
}
|
|
|
|
idleWait( howLong )
|
|
{
|
|
self endon( "end_idle" );
|
|
wait howLong;
|
|
}
|
|
|
|
idleWaitAbit()
|
|
{
|
|
self endon( "end_idle" );
|
|
wait 0.3 + randomfloat( 0.1 );
|
|
self waittill( "do_slow_things" );
|
|
}
|
|
|
|
idleThread( idlecallback )
|
|
{
|
|
self endon( "killanimscript" );
|
|
self [[ idlecallback ]]();
|
|
}
|
|
|
|
flinchWhenSuppressed( behaviorCallbacks )
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "stop_waiting_to_flinch" );
|
|
|
|
lastSuppressionTime = self.lastSuppressionTime;
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "suppression" );
|
|
|
|
time = gettime();
|
|
|
|
if ( lastSuppressionTime < time - 2000 )
|
|
break;
|
|
|
|
lastSuppressionTime = time;
|
|
}
|
|
|
|
self.flinching = true;
|
|
|
|
self thread endIdleAtFrameEnd();
|
|
|
|
assert( isdefined( behaviorCallbacks.flinch ) );
|
|
val = [[ behaviorCallbacks.flinch ]]();
|
|
|
|
if ( !val )
|
|
self notify( "dont_end_idle" );
|
|
|
|
self.flinching = false;
|
|
self notify( "flinch_done" );
|
|
}
|
|
|
|
endIdleAtFrameEnd()
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "dont_end_idle" );
|
|
waittillframeend;
|
|
|
|
if ( !isdefined( self ) )
|
|
return;
|
|
|
|
self notify( "end_idle" );
|
|
self.a.idlingAtCover = false;
|
|
}
|
|
|
|
tryThrowingGrenade( behaviorCallbacks, throwAt )
|
|
{
|
|
assert( isdefined( throwAt ) );
|
|
|
|
// don't throw backwards
|
|
forward = anglesToForward( self.angles );
|
|
dir = vectorNormalize( throwAt.origin - self.origin );
|
|
if ( vectorDot( forward, dir ) < 0 )
|
|
return false;
|
|
|
|
if ( self.doingAmbush && !recentlySawEnemy() )
|
|
return false;
|
|
|
|
if ( self isPartiallySuppressedWrapper() )
|
|
{
|
|
return callOptionalBehaviorCallback( behaviorCallbacks.grenadehidden, throwAt );
|
|
}
|
|
else
|
|
{
|
|
return callOptionalBehaviorCallback( behaviorCallbacks.grenade, throwAt );
|
|
}
|
|
}
|
|
|
|
blindfire( behaviorCallbacks )
|
|
{
|
|
if ( !canBlindFire() )
|
|
return false;
|
|
|
|
return callOptionalBehaviorCallback( behaviorCallbacks.blindfire );
|
|
}
|
|
|
|
// Need this?
|
|
breakOutOfShootingIfWantToMoveUp()
|
|
{
|
|
self endon( "killanimscript" );
|
|
self endon( "stop_deciding_how_to_shoot" );
|
|
|
|
while ( 1 )
|
|
{
|
|
if ( self.fixedNode || self.doingAmbush )
|
|
return;
|
|
|
|
wait 0.5 + randomfloat( 0.75 );
|
|
|
|
if ( !isdefined( self.enemy ) )
|
|
continue;
|
|
|
|
if ( enemyIsHiding() )
|
|
{
|
|
if ( advanceOnHidingEnemy() )
|
|
return;
|
|
}
|
|
|
|
if ( !self recentlySawEnemy() && !self canSuppressEnemy() )
|
|
{
|
|
if ( gettime() > self.a.getBoredOfThisNodeTime )
|
|
{
|
|
if ( cantFindAnythingToDo() )
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enemyIsHiding()
|
|
{
|
|
// if this function is called, we already know that our enemy is not visible from exposed.
|
|
// check to see if they're doing anything hiding-like.
|
|
|
|
if ( !isdefined( self.enemy ) )
|
|
return false;
|
|
|
|
if ( self.enemy isFlashed() )
|
|
return true;
|
|
|
|
if ( isplayer( self.enemy ) )
|
|
{
|
|
if ( isdefined( self.enemy.health ) && self.enemy.health < self.enemy.maxhealth )
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if ( isAI( self.enemy ) && self.enemy isSuppressedWrapper() )
|
|
return true;
|
|
}
|
|
|
|
if ( isdefined( self.enemy.isreloading ) && self.enemy.isreloading )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
resetRespondToDeathTime()
|
|
{
|
|
self.a.respondToDeathTime = 0;
|
|
}
|
|
|
|
|
|
resetLookForBetterCoverTime()
|
|
{
|
|
currentTime = gettime();
|
|
|
|
// treat group of shuffle nodes as one node, don't increase getBoredOfThisNodeTime by too much
|
|
if ( isdefined( self.didShuffleMove ) && currentTime > self.a.getBoredOfThisNodeTime )
|
|
{
|
|
self.a.getBoredOfThisNodeTime = currentTime + randomintrange( 2000, 5000 );
|
|
}
|
|
else if ( isdefined( self.enemy ) )
|
|
{
|
|
dist = distance2D( self.origin, self.enemy.origin );
|
|
if ( dist < self.engageMinDist )
|
|
self.a.getBoredOfThisNodeTime = currentTime + randomintrange( 5000, 10000 );
|
|
else if ( dist > self.engageMaxDist && dist < self.goalradius )
|
|
self.a.getBoredOfThisNodeTime = currentTime + randomintrange( 2000, 5000 );
|
|
else
|
|
self.a.getBoredOfThisNodeTime = currentTime + randomintrange( 10000, 15000 );
|
|
}
|
|
else
|
|
{
|
|
self.a.getBoredOfThisNodeTime = currentTime + randomintrange( 5000, 15000 );
|
|
}
|
|
}
|
|
|
|
resetSeekOutEnemyTime()
|
|
{
|
|
// we'll be willing to actually run right up to our enemy in order to find them if we haven't seen them by this time.
|
|
// however, we'll try to find better cover before seeking them out
|
|
if ( isdefined( self.aggressiveMode ) )
|
|
self.seekOutEnemyTime = gettime() + randomintrange( 500, 1000 );
|
|
else
|
|
self.seekOutEnemyTime = gettime() + randomintrange( 3000, 5000 );
|
|
}
|
|
|
|
// these next functions are "look for better cover" functions.
|
|
// they don't always need to cause the actor to leave the node immediately,
|
|
// but if they keep being called over and over they need to become more and more likely to do so,
|
|
// as this indicates that new cover is strongly needed.
|
|
cantFindAnythingToDo()
|
|
{
|
|
return advanceOnHidingEnemy();
|
|
}
|
|
|
|
advanceOnHidingEnemy()
|
|
{
|
|
if ( self.fixedNode || self.doingAmbush )
|
|
return false;
|
|
|
|
if ( isdefined( self.aggressiveMode ) && gettime() >= self.seekOutEnemyTime )
|
|
{
|
|
return tryRunningToEnemy( false );
|
|
}
|
|
|
|
foundBetterCover = false;
|
|
if ( !isdefined( self.enemy ) || !self.enemy isFlashed() )
|
|
foundBetterCover = lookForBetterCover();
|
|
|
|
if ( !foundBetterCover && isdefined( self.enemy ) && !self canSeeEnemyFromExposed() )
|
|
{
|
|
if ( gettime() >= self.seekOutEnemyTime )
|
|
{
|
|
return tryRunningToEnemy( false );
|
|
}
|
|
}
|
|
|
|
// maybe at this point we could look for someone who's suppressing our enemy,
|
|
// and if someone is, we can say "cover me!" and have them say "i got you covered" or something.
|
|
|
|
return foundBetterCover;
|
|
}
|
|
|
|
tryToGetOutOfDangerousSituation( behaviorCallbacks )
|
|
{
|
|
if ( isdefined( behaviorCallbacks.moveToNearByCover ) )
|
|
{
|
|
if ( [[ behaviorCallbacks.moveToNearByCover ]]() )
|
|
return true;
|
|
}
|
|
|
|
return lookForBetterCover();
|
|
}
|
|
|
|
// TEMP move these into animsets
|
|
set_standing_turns()
|
|
{
|
|
self.a.array[ "turn_left_45" ] = %exposed_tracking_turn45L;
|
|
self.a.array[ "turn_left_90" ] = %exposed_tracking_turn90L;
|
|
self.a.array[ "turn_left_135" ] = %exposed_tracking_turn135L;
|
|
self.a.array[ "turn_left_180" ] = %exposed_tracking_turn180L;
|
|
self.a.array[ "turn_right_45" ] = %exposed_tracking_turn45R;
|
|
self.a.array[ "turn_right_90" ] = %exposed_tracking_turn90R;
|
|
self.a.array[ "turn_right_135" ] = %exposed_tracking_turn135R;
|
|
self.a.array[ "turn_right_180" ] = %exposed_tracking_turn180R;
|
|
}
|
|
|
|
set_crouching_turns()
|
|
{
|
|
self.a.array[ "turn_left_45" ] = %exposed_crouch_turn_90_left;
|
|
self.a.array[ "turn_left_90" ] = %exposed_crouch_turn_90_left;
|
|
self.a.array[ "turn_left_135" ] = %exposed_crouch_turn_180_left;
|
|
self.a.array[ "turn_left_180" ] = %exposed_crouch_turn_180_left;
|
|
self.a.array[ "turn_right_45" ] = %exposed_crouch_turn_90_right;
|
|
self.a.array[ "turn_right_90" ] = %exposed_crouch_turn_90_right;
|
|
self.a.array[ "turn_right_135" ] = %exposed_crouch_turn_180_right;
|
|
self.a.array[ "turn_right_180" ] = %exposed_crouch_turn_180_right;
|
|
}
|
|
|
|
|
|
turnToMatchNodeDirection( nodeAngleOffset )
|
|
{
|
|
if ( isdefined( self.node ) )
|
|
{
|
|
node = self.node;
|
|
|
|
absRelYaw = abs( AngleClamp180( self.angles[1] - ( node.angles[1] + nodeAngleOffset ) ) );
|
|
|
|
if ( self.a.pose == "stand" && node getHighestNodeStance() != "stand" )
|
|
{
|
|
if ( absRelYaw > 45 && absRelYaw < 90 )
|
|
self orientmode( "face angle", self.angles[1] );
|
|
else
|
|
self orientmode( "face current" );
|
|
|
|
rate = 1.5;
|
|
noteTime = getNotetrackTimes( %exposed_stand_2_crouch, "anim_pose = \"crouch\"" )[0];
|
|
noteTime = min( 1, noteTime * 1.1 );
|
|
time = noteTime * getAnimLength( %exposed_stand_2_crouch ) / rate;
|
|
|
|
self setflaggedanimknoballrestart( "crouchanim", %exposed_stand_2_crouch, %body, 1, .2, rate );
|
|
self animscripts\shared::DoNoteTracksForTime( time, "crouchanim" );
|
|
self clearanim( %body, 0.2 );
|
|
}
|
|
|
|
self orientmode( "face angle", self.angles[1] );
|
|
|
|
relYaw = AngleClamp180( self.angles[1] - ( node.angles[1] + nodeAngleOffset ) );
|
|
|
|
if ( abs( relYaw ) > 45 )
|
|
{
|
|
if ( self.a.pose == "stand" )
|
|
set_standing_turns();
|
|
else
|
|
set_crouching_turns();
|
|
|
|
self.turnThreshold = 45;
|
|
self.turnToMatchNode = true;
|
|
animscripts\combat::TurnToFaceRelativeYaw( relYaw );
|
|
self.turnToMatchNode = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
moveToNearbyCover()
|
|
{
|
|
if ( !isdefined( self.enemy ) )
|
|
return false;
|
|
|
|
if ( isdefined( self.didShuffleMove ) )
|
|
{
|
|
self.didShuffleMove = undefined;
|
|
return false;
|
|
}
|
|
|
|
if ( !isdefined( self.node ) )
|
|
return false;
|
|
|
|
if ( randomint( 3 ) == 0 )
|
|
return false;
|
|
|
|
if ( self.fixedNode || self.doingAmbush || self.keepClaimedNode || self.keepClaimedNodeIfValid )
|
|
return false;
|
|
|
|
if ( distanceSquared( self.origin, self.node.origin ) > 16 * 16 )
|
|
return false;
|
|
|
|
node = self findshufflecovernode();
|
|
|
|
if ( isdefined( node ) && ( node != self.node ) && self useCoverNode( node ) )
|
|
{
|
|
self.shuffleMove = true;
|
|
self.shuffleNode = node;
|
|
self.didShuffleMove = true;
|
|
|
|
// give code a chance use new cover node
|
|
wait 0.5;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|