2950 lines
73 KiB
Plaintext
2950 lines
73 KiB
Plaintext
#include common_scripts\utility;
|
|
#include maps\mp\alien\_utility;
|
|
|
|
MIN_ATTACK_SEQUENCES = 6;
|
|
MAX_ATTACK_SEQUENCES = 9;
|
|
|
|
MIN_ATTACKS = 3;
|
|
MAX_ATTACKS = 6;
|
|
|
|
LEAP_OFFSET_XY = 48;
|
|
RECENT_TIME_LIMIT = 10.0;
|
|
|
|
//=======================================================
|
|
// onEnterAnimState
|
|
//=======================================================
|
|
onEnterAnimState( prevState, nextState )
|
|
{
|
|
self onExitAnimState( prevState, nextState );
|
|
|
|
self.currentAnimState = nextState;
|
|
|
|
switch ( nextState )
|
|
{
|
|
case "idle":
|
|
self maps\mp\agents\alien\_alien_idle::main();
|
|
break;
|
|
case "move":
|
|
self maps\mp\agents\alien\_alien_move::main();
|
|
break;
|
|
case "traverse":
|
|
self maps\mp\agents\alien\_alien_traverse::main();
|
|
break;
|
|
case "melee":
|
|
self maps\mp\agents\alien\_alien_melee::main();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
//=======================================================
|
|
// onExitAnimState
|
|
//=======================================================
|
|
onExitAnimState( prevState, nextState )
|
|
{
|
|
self notify( "killanimscript" );
|
|
|
|
switch( prevState )
|
|
{
|
|
case "idle":
|
|
self maps\mp\agents\alien\_alien_idle::end_script();
|
|
break;
|
|
case "move":
|
|
self maps\mp\agents\alien\_alien_move::end_script();
|
|
break;
|
|
case "traverse":
|
|
self maps\mp\agents\alien\_alien_traverse::end_script();
|
|
break;
|
|
case "melee":
|
|
self maps\mp\agents\alien\_alien_melee::end_script();
|
|
break;
|
|
}
|
|
}
|
|
|
|
MonitorFlash()
|
|
{
|
|
self endon( "death" );
|
|
while ( true )
|
|
{
|
|
self waittill( "flashbang", origin, percent_distance, percent_angle, attacker, teamName, extraDuration );
|
|
switch ( self.currentAnimState )
|
|
{
|
|
case "idle":
|
|
break;
|
|
case "move":
|
|
self maps\mp\agents\alien\_alien_move::onFlashbanged();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//=======================================================
|
|
// onDamaged
|
|
//=======================================================
|
|
onDamageFinish( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, timeOffset )
|
|
{
|
|
// Apply the actual damage
|
|
self FinishAgentDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, timeOffset, 0.0 );
|
|
|
|
// death script will handle things.
|
|
if ( self.health <= 0 )
|
|
return true;
|
|
|
|
trap_damage = self is_trap( eInflictor );
|
|
|
|
registerDamage( iDamage );
|
|
|
|
if ( isDefined( eAttacker ) )
|
|
{
|
|
if ( isPlayer( eAttacker ) || ( isDefined( eAttacker.owner ) && isPlayer( eAttacker.owner ) ) )
|
|
{
|
|
if( !trap_damage )
|
|
eAttacker maps\mp\alien\_damage::check_for_special_damage( self, sWeapon , sMeansOfDeath); //play some FX if specialized ammo is used
|
|
|
|
if ( iDamage > 0 )
|
|
level.alienBBData[ "damage_done" ] += iDamage;
|
|
}
|
|
}
|
|
|
|
is_stun = should_do_stun_damage( sWeapon, sMeansOfDeath, eAttacker );
|
|
|
|
if ( !is_stun )
|
|
{
|
|
belowPainThreshold = self belowCumulativePainThreshold( eAttacker,sWeapon );
|
|
|
|
// If we haven't hit the pain threshold, return
|
|
if ( sMeansOfDeath != "MOD_MELEE" && belowPainThreshold )
|
|
return;
|
|
|
|
if ( sMeansOfDeath == "MOD_MELEE" && !belowPainThreshold && !should_melee_pushback() )
|
|
return;
|
|
}
|
|
|
|
if ( !isDefined ( vDir ) )
|
|
vDir = anglesToForward( self.angles );
|
|
|
|
notifyJumpPain( vDir, sHitLoc, iDamage, is_stun );
|
|
clearDamageHistory();
|
|
|
|
if ( is_stun && !trap_damage )
|
|
{
|
|
self thread maps\mp\alien\_alien_fx::fx_stun_damage();
|
|
iDFlags = iDFlags | level.iDFLAGS_STUN;
|
|
}
|
|
|
|
|
|
switch ( self.currentAnimState )
|
|
{
|
|
case "idle":
|
|
self maps\mp\agents\alien\_alien_idle::onDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, timeOffset );
|
|
break;
|
|
case "move":
|
|
self maps\mp\agents\alien\_alien_move::onDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, timeOffset );
|
|
break;
|
|
case "melee":
|
|
self maps\mp\agents\alien\_alien_melee::onDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, timeOffset );
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
should_do_stun_damage( sWeapon, sMeansofDeath, eAttacker )
|
|
{
|
|
if ( self get_alien_type() == "elite" )
|
|
return false;
|
|
|
|
if ( is_true( self.is_burning ) )
|
|
return false;
|
|
|
|
if ( isDefined ( eAttacker ) && isplayer( eAttacker ) && sMeansofDeath != "MOD_MELEE" )
|
|
{
|
|
using_primary = isDefined( sWeapon ) && ( sWeapon == eAttacker GetCurrentPrimaryWeapon() );
|
|
|
|
return using_primary && ( eAttacker maps\mp\alien\_utility::has_stun_ammo() );
|
|
}
|
|
return false;
|
|
}
|
|
|
|
should_melee_pushback()
|
|
{
|
|
if ( self get_alien_type() == "elite" )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
notifyJumpPain( damageDirection, hitLocation, iDamage, stun )
|
|
{
|
|
//<NOTE J.C.> Since jump does not tie with any particular anim states and it requires specific timing, we cannot do the
|
|
// jump pain the same as the onDamage() call above.
|
|
self notify ( "jump_pain", damageDirection, hitLocation, iDamage, stun );
|
|
}
|
|
|
|
clearDamageHistory()
|
|
{
|
|
self.recentDamages = [];
|
|
self.damageListIndex = 0;
|
|
}
|
|
|
|
registerDamage( iDamage )
|
|
{
|
|
damageInfo = [];
|
|
damageInfo [ "amount" ] = iDamage;
|
|
damageInfo [ "time" ] = getTime();
|
|
|
|
self.recentDamages [ self.damageListIndex ] = damageInfo;
|
|
self.damageListIndex ++;
|
|
|
|
if ( self.damageListIndex == level.damageListSize )
|
|
self.damageListIndex = 0;
|
|
}
|
|
|
|
belowCumulativePainThreshold( eAttacker,sWeapon )
|
|
{
|
|
alien_type = get_alien_type();
|
|
recentCumulativeDamage = getCumulativeDamage();
|
|
|
|
threshold = level.alien_types[ alien_type ].attributes[ "min_cumulative_pain_threshold" ];
|
|
|
|
return ( recentCumulativeDamage < threshold );
|
|
}
|
|
|
|
getCumulativeDamage()
|
|
{
|
|
alien_type = get_alien_type();
|
|
lookBackTime = level.alien_types[ alien_type ].attributes[ "min_cumulative_pain_buffer_time" ] * 1000.0; //in ms. If a damage is older than this value, it is not considered to be "recent" any more
|
|
|
|
recentCumDamage = 0;
|
|
currentTime = getTime();
|
|
for ( i = 0; i < self.recentDamages.size; i++ )
|
|
{
|
|
if ( currentTime - self.recentDamages[ i ][ "time" ] < lookBackTime )
|
|
recentCumDamage += self.recentDamages[ i ][ "amount" ];
|
|
}
|
|
return recentCumDamage;
|
|
}
|
|
|
|
// timeFromNow is how far in the future from now (in seconds) to predict
|
|
GetTargetPredictedPosition( target, timeFromNow )
|
|
{
|
|
targetVel = target GetEntityVelocity();
|
|
predictedPos = target.origin + ( timeFromNow * targetVel );
|
|
|
|
return predictedPos;
|
|
}
|
|
|
|
GetAttackPosition( target, attackDist, timeFromNow )
|
|
{
|
|
alienRadius = 15;
|
|
|
|
targetPredictedPosition = self GetTargetPredictedPosition( target, timeFromNow );
|
|
|
|
targetToMe = VectorNormalize( self.origin - targetPredictedPosition );
|
|
attackPos = targetPredictedPosition + targetToMe * attackDist;
|
|
|
|
attackPos = GetGroundPosition( attackPos, alienRadius, 64, 64 );
|
|
|
|
return attackPos;
|
|
}
|
|
|
|
// logic filling in the hole left by actor_alien_exposed code.
|
|
AttemptMelee()
|
|
{
|
|
if ( !IsDefined( self.enemy ) )
|
|
return false;
|
|
|
|
self ScrAgentBeginMelee( self.enemy );
|
|
return true;
|
|
}
|
|
|
|
|
|
// This file should have NO animation logic. It is not an animscript.
|
|
|
|
ALIEN_LEAP_MELEE_DISTANCE_MAX = 400;
|
|
ALIEN_APPROACH_DIST_SQR = 250000.0; // 500.0 * 500.0;
|
|
ALIEN_JOG_CHANGE_DISTANCE = 2000;
|
|
ALIEN_LEAP_MELEE_DISTANCE_MIN = 208;
|
|
ALIEN_MIN_MELEE_DISTANCE = 100;
|
|
ALIEN_DISTANCE_DO_DAMAGE_WHEN_RETREAT = 256;
|
|
ALIEN_PROXIMITY_CHECK_ACTIVATION_DISTANCE = 256;
|
|
ALIEN_GOAL_RADIUS_ADJUSTMENT = -48;
|
|
ALIEN_NODE_GOAL_RADIUS = 64;
|
|
|
|
main()
|
|
{
|
|
self endon( "death" );
|
|
|
|
waitframe(); // Not allowed to set goals on the first frame of an actor's existence
|
|
|
|
debugTestDome = false;
|
|
debugTestEnvPrototype = false;
|
|
|
|
if ( should_react_to_downed_enemies() )
|
|
self thread downed_enemy_monitor();
|
|
|
|
while ( 1 )
|
|
{
|
|
if ( debugTestDome )
|
|
self alien_test_loop();
|
|
else if ( debugTestEnvPrototype )
|
|
self alien_test_jump();
|
|
else
|
|
self alien_main_loop();
|
|
}
|
|
}
|
|
|
|
should_react_to_downed_enemies()
|
|
{
|
|
switch ( get_alien_type() )
|
|
{
|
|
case "elite":
|
|
case "spitter":
|
|
case "seeder":
|
|
case "minion":
|
|
case "gargoyle":
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
downed_enemy_monitor()
|
|
{
|
|
self endon( "death" );
|
|
|
|
while ( true )
|
|
{
|
|
if ( !IsPlayer( self.enemy ) )
|
|
{
|
|
wait 0.05;
|
|
continue;
|
|
}
|
|
|
|
self thread monitor_enemy_for_downed( self.enemy );
|
|
self common_scripts\utility::waittill_any( "downed_enemy", "enemy" );
|
|
}
|
|
}
|
|
|
|
monitor_enemy_for_downed( enemy )
|
|
{
|
|
self endon( "enemy" );
|
|
self endon( "death" );
|
|
|
|
enemy waittill_any( "death", "last_stand" );
|
|
|
|
MAX_DISTANCE_FOR_DOWNED_SQ = 256 * 256;
|
|
|
|
if ( DistanceSquared( enemy.origin, self.origin ) < MAX_DISTANCE_FOR_DOWNED_SQ )
|
|
{
|
|
self.enemy_downed = true;
|
|
self.downed_enemy_location = enemy.origin;
|
|
}
|
|
|
|
self notify( "downed_enemy" );
|
|
}
|
|
|
|
|
|
alien_test_loop()
|
|
{
|
|
movePosList =
|
|
[
|
|
( -296, 448, -352 ),
|
|
( 808, 300, -368 ),
|
|
( 1320.6, 482.8, -320 )
|
|
//( 416, -384, -368 ),
|
|
//( 413.5, 363.1, -364 ),
|
|
//( 1194.3, 36.1, -354 ),
|
|
//( 848, -456, -352 ),
|
|
//( 280, -16, -360 )
|
|
];
|
|
|
|
wait( 10 );
|
|
|
|
while ( true )
|
|
{
|
|
//movePosList = array_randomize( movePosList );
|
|
|
|
foreach( pos in movePosList )
|
|
{
|
|
if ( DistanceSquared( self.origin, pos ) < 100 )
|
|
continue;
|
|
|
|
self ScrAgentSetGoalPos( pos );
|
|
self waittill( "goal_reached" );
|
|
wait( 3 );
|
|
}
|
|
}
|
|
}
|
|
|
|
alien_test_jump()
|
|
{
|
|
movePosList =
|
|
[
|
|
( -203.3, -898, 151.03304 ),
|
|
( -1282.2, -669.1, -26.529 )
|
|
];
|
|
|
|
wait( 10 );
|
|
|
|
while ( true )
|
|
{
|
|
//movePosList = array_randomize( movePosList );
|
|
|
|
foreach( pos in movePosList )
|
|
{
|
|
if ( DistanceSquared( self.origin, pos ) < 100 )
|
|
continue;
|
|
|
|
self ScrAgentSetGoalPos( pos );
|
|
self waittill( "goal_reached" );
|
|
wait( 3 );
|
|
}
|
|
}
|
|
}
|
|
|
|
handle_attractor_flare( flare, is_active )
|
|
{
|
|
if ( is_active )
|
|
{
|
|
self.attractor_flare = flare;
|
|
}
|
|
else
|
|
{
|
|
if ( !IsDefined( self.attractor_flare ) )
|
|
return;
|
|
|
|
if ( IsDefined( flare ) && flare != self.attractor_flare )
|
|
return;
|
|
|
|
self.attractor_flare = undefined;
|
|
}
|
|
|
|
self notify( "alien_main_loop_restart" );
|
|
}
|
|
|
|
react_to_attractor_flare()
|
|
{
|
|
MIN_WAIT_TIME = 3.0;
|
|
MAX_WAIT_TIME = 6.0;
|
|
|
|
attractNode = get_flare_node();
|
|
if ( IsDefined( attractNode ) )
|
|
{
|
|
self ScrAgentSetGoalNode( attractNode );
|
|
self ScrAgentSetGoalRadius( 64.0 );
|
|
self waittill( "goal_reached" );
|
|
|
|
while ( IsDefined( self.attractor_flare ) )
|
|
wait 0.2;
|
|
}
|
|
}
|
|
|
|
get_flare_node()
|
|
{
|
|
attractNodes = GetNodesInRadius( self.attractor_flare.origin, 300, 50, 128, "path" );
|
|
|
|
if ( attractNodes.size == 0)
|
|
return undefined;
|
|
|
|
inBetweenNodes = [];
|
|
flareToAlien = VectorNormalize( self.origin - self.attractor_flare.origin );
|
|
|
|
foreach ( possibleNode in attractNodes )
|
|
{
|
|
flareToNode = VectorNormalize( possibleNode.origin - self.attractor_flare.origin );
|
|
if ( VectorDot( flareToNode, flareToAlien ) < 0 )
|
|
continue;
|
|
|
|
inBetweenNodes[inBetweenNodes.size] = possibleNode;
|
|
}
|
|
|
|
if ( inBetweenNodes.size == 0 )
|
|
return attractNodes[ RandomInt( attractNodes.size ) ];
|
|
|
|
return inBetweenNodes[ RandomInt( inBetweenNodes.size ) ];
|
|
}
|
|
|
|
alien_main_loop()
|
|
{
|
|
self endon ( "alien_main_loop_restart" );
|
|
while ( 1 )
|
|
{
|
|
self.looktarget = undefined;
|
|
enemy = self.enemy;
|
|
if ( IsDefined( self.alien_scripted ) && self.alien_scripted == true )
|
|
{
|
|
// Do no logic
|
|
}
|
|
else if ( IsDefined( self.attractor_flare ) )
|
|
{
|
|
react_to_attractor_flare();
|
|
}
|
|
else if ( IsDefined( self.enemy_downed ) && self.enemy_downed )
|
|
{
|
|
posture_after_enemy_downed();
|
|
self.enemy_downed = false;
|
|
self.downed_enemy_location = undefined;
|
|
}
|
|
else if ( IsAlive( enemy ) )
|
|
{
|
|
if ( self.badpath )
|
|
{
|
|
[[ level.alien_funcs[get_alien_type()]["badpath"] ]] ( enemy );
|
|
}
|
|
else
|
|
{
|
|
[[ level.alien_funcs[get_alien_type()]["combat"] ]] ( enemy );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( isdefined( self.pet ) && self.pet )
|
|
{
|
|
alien_pet_follow();
|
|
}
|
|
else
|
|
{
|
|
alien_noncombat();
|
|
}
|
|
}
|
|
wait 0.05;
|
|
}
|
|
}
|
|
|
|
posture_after_enemy_downed()
|
|
{
|
|
self endon( "damage" );
|
|
self endon( "enemy_downed_proximity_breached" );
|
|
|
|
MIN_WAIT_TIME = 4.0;
|
|
MAX_WAIT_TIME = 6.0;
|
|
|
|
postureNode = get_downed_posture_node();
|
|
|
|
if ( IsDefined( postureNode ) )
|
|
{
|
|
self ScrAgentSetGoalNode( postureNode );
|
|
self ScrAgentSetGoalRadius( 64.0 );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
|
|
self thread monitor_enemy_proximity_during_enemy_downed();
|
|
wait RandomFloatRange( MIN_WAIT_TIME, MAX_WAIT_TIME );
|
|
}
|
|
|
|
monitor_enemy_proximity_during_enemy_downed()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "damage" );
|
|
self endon ( "alien_main_loop_restart" );
|
|
|
|
ENEMY_PROXIMITY_RANGE_SQ = 128 * 128;
|
|
|
|
while ( true )
|
|
{
|
|
foreach ( player in level.players )
|
|
{
|
|
if ( DistanceSquared( player.origin, self.origin ) < ENEMY_PROXIMITY_RANGE_SQ )
|
|
{
|
|
self notify( "enemy_downed_proximity_breached" );
|
|
return;
|
|
}
|
|
}
|
|
|
|
wait 0.1;
|
|
}
|
|
}
|
|
|
|
get_downed_posture_node()
|
|
{
|
|
postureNodes = GetNodesInRadius( self.origin, 300, 50, 128, "path" );
|
|
if ( postureNodes.size == 0 )
|
|
{
|
|
return; // Retreat failed
|
|
}
|
|
|
|
filters = [];
|
|
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_override" ] = VectorNormalize( self.origin - self.downed_enemy_location );
|
|
filters[ "direction_weight" ] = 10.0;
|
|
filters[ "min_height" ] = 64.0;
|
|
filters[ "max_height" ] = 400.0;
|
|
filters[ "height_weight" ] = 0.0;
|
|
filters[ "enemy_los" ] = false;
|
|
filters[ "enemy_los_weight" ] = 0.0;
|
|
filters[ "min_dist_from_enemy" ] = 400.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
filters[ "desired_dist_from_enemy" ] = 600.0;
|
|
filters[ "dist_from_enemy_weight" ] = 0.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 200.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 6.0;
|
|
filters[ "not_recently_used_weight" ] = 2.0;
|
|
filters[ "random_weight" ] = 1.0;
|
|
|
|
result = get_retreat_node_rated( self.enemy, filters, postureNodes );
|
|
|
|
return result;
|
|
}
|
|
|
|
default_alien_combat( enemy )
|
|
{
|
|
enemy endon( "death" );
|
|
self endon( "enemy" );
|
|
self endon( "bad_path" );
|
|
|
|
self clear_attacking( enemy );
|
|
|
|
while ( IsAlive( enemy ) )
|
|
{
|
|
self.looktarget = enemy;
|
|
if ( should_i_attack( enemy ))
|
|
{
|
|
self set_attacking( enemy );
|
|
self alien_attack_enemy( enemy );
|
|
self clear_attacking( enemy );
|
|
|
|
if ( should_i_retreat( enemy ) )
|
|
alien_retreat( enemy );
|
|
}
|
|
else
|
|
{
|
|
alien_wait_for_combat( enemy );
|
|
}
|
|
}
|
|
}
|
|
|
|
alien_attack_enemy( enemy )
|
|
{
|
|
if ( use_synched_attack ( enemy ) )
|
|
self alien_synch_attack_enemy( enemy );
|
|
else
|
|
self alien_attack_sequence( enemy );
|
|
}
|
|
|
|
use_synched_attack( enemy )
|
|
{
|
|
switch ( get_alien_type() )
|
|
{
|
|
case "elite":
|
|
case "spitter":
|
|
case "seeder":
|
|
case "minion":
|
|
case "locust":
|
|
case "gargoyle":
|
|
case "mammoth":
|
|
return false;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return IsDefined( enemy.synch_attack_setup );
|
|
}
|
|
|
|
alien_attack_sequence( enemy )
|
|
{
|
|
num_attack_sequences = get_attack_sequence_num();
|
|
for ( attack_sequences = 0; attack_sequences < num_attack_sequences; attack_sequences++ )
|
|
{
|
|
// JohnW: Removing close_range_retreat for now in lieu of move_side animations
|
|
//if ( attack_sequences > 0 )
|
|
//close_range_retreat( enemy );
|
|
|
|
num_attacks = get_attack_num();
|
|
for ( attack = 0; attack < num_attacks; attack++ )
|
|
{
|
|
if ( has_attack_abort_been_requested( self ) )
|
|
return;
|
|
|
|
attack_type = [[ level.alien_funcs[get_alien_type()]["approach"] ]] ( enemy, attack ); // Approach enemy and decide on melee type, if any
|
|
|
|
alien_attack( enemy, attack_type );
|
|
|
|
// Only allow subsequent melee's if we're at least 64 units away from the player
|
|
if ( DistanceSquared( self.origin, self.enemy.origin ) < 64*64 )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
alien_synch_attack_enemy( enemy )
|
|
{
|
|
if ( Distance2DSquared( self.origin, enemy.origin ) > ALIEN_MIN_MELEE_DISTANCE )
|
|
{
|
|
self ScrAgentSetGoalRadius( ALIEN_MIN_MELEE_DISTANCE );
|
|
self ScrAgentSetGoalEntity( enemy );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
|
|
hasValidSynchAttacker = ( IsAlive( enemy.synch_attack_setup.primary_attacker ) && enemy.synch_attack_setup.primary_attacker != self );
|
|
canbeAttacked = true;
|
|
if ( IsDefined( enemy.synch_attack_setup.can_synch_attack_func ) )
|
|
canbeAttacked = enemy [[enemy.synch_attack_setup.can_synch_attack_func]]();
|
|
|
|
if ( !hasValidSynchAttacker && canbeAttacked )
|
|
{
|
|
enemy.synch_attack_setup.primary_attacker = self;
|
|
select_synch_index( enemy );
|
|
|
|
if ( should_move_to_synch_attack_pos( enemy ) )
|
|
{
|
|
self ScrAgentSetGoalRadius( 30 );
|
|
self ScrAgentSetGoalPos( self.synch_attack_pos );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
|
|
synch_melee( enemy );
|
|
}
|
|
else
|
|
{
|
|
swipe_static_melee( enemy );
|
|
}
|
|
}
|
|
|
|
select_synch_index( enemy )
|
|
{
|
|
synchList = enemy get_synch_direction_list( self );
|
|
enemyToSelf = VectorNormalize( self.origin - enemy.origin );
|
|
bestSynchAttackIndex = undefined;
|
|
bestSynchAnimState = undefined;
|
|
bestSynchAttackPos = undefined;
|
|
bestOffsetAngle = -1.1;
|
|
|
|
foreach ( index, synchPos in synchList )
|
|
{
|
|
synchAnimState = synchPos["attackerAnimState"];
|
|
attackAnim = self GetAnimEntry( synchAnimState , 0 );
|
|
|
|
offsetPos = GetStartOrigin( enemy.origin, enemy.angles, attackAnim );
|
|
offset = Length( offsetPos - enemy.origin );
|
|
rotatedOffset = synchPos["offset_direction"] * offset;
|
|
synchAttackPos = enemy LocalToWorldCoords( rotatedOffset );
|
|
|
|
enemyToSynchPos = VectorNormalize( synchAttackPos - enemy.origin );
|
|
offsetAngle = VectorDot( enemyToSynchPos, enemyToSelf );
|
|
|
|
if ( offsetAngle > bestOffsetAngle )
|
|
{
|
|
bestSynchAttackIndex = index;
|
|
bestSynchAnimState = synchAnimState;
|
|
bestSynchAttackPos = synchAttackPos;
|
|
bestOffsetAngle = offsetAngle;
|
|
}
|
|
}
|
|
|
|
self.synch_attack_index = bestSynchAttackIndex;
|
|
self.synch_anim_state = bestSynchAnimState;
|
|
self.synch_attack_pos = bestSynchAttackPos;
|
|
}
|
|
|
|
should_move_to_synch_attack_pos( enemy )
|
|
{
|
|
COS_45 = 0.707;
|
|
|
|
enemyToPos = VectorNormalize( self.synch_attack_pos - enemy.origin );
|
|
enemyToSelf = VectorNormalize( self.origin - enemy.origin );
|
|
isInSameDirection = VectorDot( enemyToPos, enemyToSelf ) > COS_45;
|
|
isFurther = DistanceSquared( enemy.origin, self.origin ) > DistanceSquared( enemy.origin, self.synch_attack_pos );
|
|
|
|
return isFurther || !isInSameDirection;
|
|
}
|
|
|
|
alien_attack( enemy, attack_type )
|
|
{
|
|
self notify( "start_attack" );
|
|
|
|
switch ( attack_type )
|
|
{
|
|
case "swipe":
|
|
swipe_melee( enemy );
|
|
break;
|
|
case "leap":
|
|
leap_melee( enemy );
|
|
break;
|
|
case "wall":
|
|
wall_leap_melee( enemy );
|
|
break;
|
|
case "badpath_jump":
|
|
badpath_jump( enemy );
|
|
break;
|
|
case "spit":
|
|
maps\mp\agents\alien\_alien_spitter::spit_projectile( enemy );
|
|
break;
|
|
case "charge":
|
|
maps\mp\agents\alien\_alien_elite::charge_attack( enemy );
|
|
break;
|
|
case "slam":
|
|
maps\mp\agents\alien\_alien_elite::ground_slam( enemy );
|
|
break;
|
|
case "angered":
|
|
maps\mp\agents\alien\_alien_elite::angered( enemy );
|
|
break;
|
|
case "synch":
|
|
synch_melee( enemy );
|
|
break;
|
|
case "swipe_static":
|
|
swipe_static_melee( enemy );
|
|
break;
|
|
case "explode":
|
|
maps\mp\agents\alien\_alien_minion::explode_attack( enemy );
|
|
case "none":
|
|
// Couldn't get in range for melee
|
|
break;
|
|
default:
|
|
if ( isDefined( level.alien_attack_override_func ) )
|
|
if ( [[level.alien_attack_override_func]]( enemy, attack_type ) )
|
|
break;
|
|
|
|
/# iprintln( "Invalid alien attack_type: " + attack_type ); #/
|
|
wait 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
MAX_BADPATH_MELEE_NODE_DISTANCE = 256;
|
|
MAX_BADPATH_SWIPE_HEIGHT_DIFFERENCE = 50;
|
|
MAX_BADPATH_JUMP_HEIGHT_DIFFERENCE = 128;
|
|
MAX_BADPATH_LEAP_DROP_DISTANCE = 256; // Significantly longer drop distance for badpath
|
|
MAX_BADPATH_MELEE_DISTANCE = 150;
|
|
|
|
handle_badpath( enemy )
|
|
{
|
|
enemy endon( "death" );
|
|
enemy endon( "disconnect" );
|
|
self endon( "bad_path" );
|
|
/# debug_alien_ai_state( "handle_badpath" ); #/
|
|
self.badpath = false;
|
|
|
|
if ( !can_attempt_badpath_move() )
|
|
return;
|
|
|
|
if ( self.badpathcount > 3 )
|
|
{
|
|
if ( attempt_bad_path_move_nearby_node( enemy ) )
|
|
self.badpathcount = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
if ( !should_bad_path_melee() )
|
|
return;
|
|
|
|
if ( self.badpathcount == 1 )
|
|
moveSuccess = attempt_badpath_move_to_node( enemy, 0, ALIEN_MIN_MELEE_DISTANCE, MAX_BADPATH_SWIPE_HEIGHT_DIFFERENCE );
|
|
else
|
|
moveSuccess = false;
|
|
|
|
if ( !moveSuccess )
|
|
{
|
|
moveSuccess = attempt_badpath_move_to_node( enemy, ALIEN_MIN_MELEE_DISTANCE, MAX_BADPATH_MELEE_NODE_DISTANCE, MAX_BADPATH_JUMP_HEIGHT_DIFFERENCE );
|
|
if ( !moveSuccess )
|
|
return;
|
|
}
|
|
|
|
if ( attempt_bad_path_melee( enemy ) )
|
|
self.badpathcount = 0;
|
|
}
|
|
|
|
can_attempt_badpath_move()
|
|
{
|
|
switch ( self.currentAnimState )
|
|
{
|
|
case "traverse":
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
should_bad_path_melee()
|
|
{
|
|
switch ( get_alien_type() )
|
|
{
|
|
case "spitter":
|
|
case "seeder":
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
attempt_bad_path_move_nearby_node( enemy )
|
|
{
|
|
MIN_JUMP_NODE_DISTANCE = 128;
|
|
MAX_JUMP_NODE_DISTANCE = 512;
|
|
|
|
nodes = GetNodesInRadius( self.origin, MAX_JUMP_NODE_DISTANCE, MIN_JUMP_NODE_DISTANCE, 256, "node_pathnode" );
|
|
if ( nodes.size == 0 )
|
|
return false;
|
|
|
|
self ScrAgentSetGoalPos( self.origin);
|
|
self ScrAgentSetGoalRadius( 2048 );
|
|
targetNode = nodes[ RandomInt( nodes.size ) ];
|
|
if ( self attempt_badpath_jump( enemy, targetNode ) )
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
self ScrAgentSetGoalNode( targetNode );
|
|
self ScrAgentSetGoalRadius( 64 );
|
|
self waittill( "goal_reached" );
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
attempt_bad_path_melee( enemy )
|
|
{
|
|
heightDifference = abs( self.origin[2] - enemy.origin[2] );
|
|
distanceToEnemy = Distance2D( self.origin, enemy.origin );
|
|
|
|
if ( distanceToEnemy > MAX_BADPATH_MELEE_NODE_DISTANCE )
|
|
return false;
|
|
|
|
canSeeEnemy = self AgentCanSeeSentient( enemy );
|
|
|
|
if ( canSeeEnemy && heightDifference <= MAX_BADPATH_SWIPE_HEIGHT_DIFFERENCE && distanceToEnemy <= MAX_BADPATH_MELEE_DISTANCE )
|
|
{
|
|
self.bad_path_handled = true;
|
|
|
|
if ( maps\mp\alien\_utility::should_explode() )
|
|
{
|
|
alien_attack( enemy, "explode" );
|
|
return true;
|
|
}
|
|
else if ( is_normal_upright( AnglesToUp( self.angles ) ) )
|
|
{
|
|
alien_attack( enemy, "swipe" );
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
self.leapEndPos = self maps\mp\agents\alien\_alien_melee::get_leap_end_pos( 1.0, LEAP_OFFSET_XY, enemy );
|
|
leapEndGroundPos = maps\mp\agents\_scriptedAgents::DropPosToGround( self.leapEndPos, MAX_BADPATH_LEAP_DROP_DISTANCE ); // Traces down
|
|
|
|
// If we found no valid ground under the end node, don't jump
|
|
if ( !IsDefined( leapEndGroundPos ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If we can't jump between these points without hitting geo, don't jump
|
|
if ( !TrajectoryCanAttemptAccurateJump( self.origin, AnglesToUp( self.angles ), self.leapEndPos, AnglesToUp( enemy.angles ), level.alien_jump_melee_gravity, 1.01 * level.alien_jump_melee_speed ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
nodes = GetNodesInRadiusSorted( leapEndGroundPos, 256, 0, 256 );
|
|
if ( !nodes.size )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If we can't navigate back to the enemy from the jump end position, don't jump
|
|
pathdist = GetPathDist( leapEndGroundPos, nodes[0].origin, 512 );
|
|
if ( pathDist < 0 )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
leap_melee( enemy );
|
|
return true;
|
|
|
|
}
|
|
|
|
attempt_badpath_move_to_node( enemy, min_distance, max_distance, height_difference )
|
|
{
|
|
nodes = GetNodesInradius( enemy.origin, max_distance, min_distance, height_difference );
|
|
|
|
if( nodes.size > 0 )
|
|
{
|
|
moveNode = nodes[ RandomInt( nodes.size ) ];
|
|
self ScrAgentSetGoalNode( moveNode );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill ( "goal_reached" );
|
|
return true;
|
|
}
|
|
|
|
wait 0.1;
|
|
return false;
|
|
}
|
|
|
|
attempt_badpath_jump( enemy, node )
|
|
{
|
|
traceResult = TrajectoryCanAttemptAccurateJump( self.origin, AnglesToUp( self.angles ), node.origin, AnglesToUp( node.angles ), level.alien_jump_melee_gravity, 1.01 * level.alien_jump_melee_speed );
|
|
|
|
if ( traceResult || self.oriented ) // The trace will almost always fail for oriented actors, so let them attempt the badPath jump
|
|
{
|
|
self.leapEndPos = node.origin;
|
|
self.leapEndAngles = node.angles;
|
|
self alien_attack( enemy, "badpath_jump" );
|
|
self ScrAgentSetGoalPos( self.origin );
|
|
self ScrAgentSetGoalRadius( 2048 );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
wait 0.5;
|
|
|
|
return traceResult;
|
|
}
|
|
|
|
restart_loop_near_enemy( enemy )
|
|
{
|
|
self endon( "alien_main_loop_restart" );
|
|
enemy endon( "death" );
|
|
self endon( "death" );
|
|
self endon( "bad_path" );
|
|
|
|
DIST_TO_ENEMY_SQR = 256.0 * 256.0;
|
|
while ( 1 )
|
|
{
|
|
if ( DistanceSquared( self.origin, enemy.origin ) < DIST_TO_ENEMY_SQR )
|
|
{
|
|
self notify( "alien_main_loop_restart" );
|
|
}
|
|
wait 0.05;
|
|
}
|
|
|
|
}
|
|
|
|
set_attacking( enemy )
|
|
{
|
|
assert( !array_contains( enemy.current_attackers, self ) );
|
|
|
|
enemy.current_attackers[ enemy.current_attackers.size ] = self;
|
|
self.attacking_player = true;
|
|
self.bypass_max_attacker_counter = false;
|
|
}
|
|
|
|
clear_attacking( enemy )
|
|
{
|
|
if( !isDefined ( enemy.current_attackers ) )
|
|
enemy.current_attackers = [];
|
|
|
|
enemy.current_attackers = array_remove( enemy.current_attackers, self );
|
|
self.attacking_player = false;
|
|
self.abort_attack_requested = false;
|
|
}
|
|
|
|
clean_up_attackers()
|
|
{
|
|
new_attackers = [];
|
|
foreach ( enemy in self.current_attackers )
|
|
{
|
|
if ( isAlive( enemy ) && isAlive( enemy.enemy ) && enemy.enemy == self )
|
|
{
|
|
new_attackers[ new_attackers.size ] = enemy;
|
|
}
|
|
}
|
|
|
|
self.current_attackers = new_attackers;
|
|
}
|
|
|
|
should_i_attack( enemy )
|
|
{
|
|
// if we're idle state locked, it can be overridden by a melee attack, so only prevent melee if state locked and not idle state locked
|
|
if( !is_idle_state_locked() && self.stateLocked )
|
|
return false;
|
|
|
|
enemy clean_up_attackers();
|
|
|
|
return true;
|
|
|
|
// JohnW: Turning off max attacker system
|
|
/*currentAttackerValue = get_current_attacker_value( enemy );
|
|
if ( ( currentAttackerValue + level.alien_types[ self.alien_type ].attributes[ "attacker_difficulty" ] ) < level.maxAlienAttackerDifficultyValue
|
|
|| ( isDefined ( self.bypass_max_attacker_counter ) && self.bypass_max_attacker_counter == true ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
attackersToReplace = find_attacker_to_replace( enemy, currentAttackerValue );
|
|
if ( attackersToReplace.size > 0 )
|
|
{
|
|
foreach ( attacker in attackersToReplace )
|
|
replace_attacker_request( attacker );
|
|
return true;
|
|
}
|
|
|
|
return false;*/
|
|
}
|
|
|
|
should_i_retreat( enemy )
|
|
{
|
|
if ( IsDefined( level.dlc_alien_can_retreat_override_func ) )
|
|
{
|
|
if ( ![[level.dlc_alien_can_retreat_override_func]]( enemy ) )
|
|
return false;
|
|
}
|
|
|
|
switch( get_alien_type())
|
|
{
|
|
case "elite":
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
replace_attacker_request( attacker )
|
|
{
|
|
attacker.abort_attack_requested = true;
|
|
}
|
|
|
|
has_attack_abort_been_requested( attacker )
|
|
{
|
|
return IsDefined( attacker.abort_attack_requested ) && attacker.abort_attack_requested;
|
|
}
|
|
|
|
find_attacker_to_replace( enemy, current_attacker_value )
|
|
{
|
|
priority = level.alien_types[ self.alien_type ].attributes[ "attacker_priority" ];
|
|
requiredReplaceValue = level.alien_types[ self.alien_type ].attributes[ "attacker_difficulty" ] - ( level.maxAlienAttackerDifficultyValue - current_attacker_value );
|
|
replacedAttackers = [];
|
|
foreach ( attacker in enemy.current_attackers )
|
|
{
|
|
if ( has_attack_abort_been_requested( attacker ) )
|
|
continue;
|
|
|
|
if ( priority < level.alien_types[ attacker.alien_type ].attributes[ "attacker_priority" ] )
|
|
{
|
|
requiredReplaceValue -= level.alien_types[ attacker.alien_type ].attributes[ "attacker_difficulty" ];
|
|
replacedAttackers[ replacedAttackers.size ] = attacker;
|
|
|
|
if ( requiredReplaceValue <= 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return replacedAttackers;
|
|
}
|
|
|
|
get_current_attacker_value( enemy )
|
|
{
|
|
attackerValue = 0.0;
|
|
if ( !IsDefined( enemy.current_attackers ) )
|
|
return attackerValue;
|
|
|
|
foreach ( attacker in enemy.current_attackers )
|
|
{
|
|
if ( !IsAlive( attacker ) )
|
|
continue;
|
|
|
|
if ( has_attack_abort_been_requested( attacker ) )
|
|
continue;
|
|
|
|
attackerValue += level.alien_types[ attacker.alien_type ].attributes[ "attacker_difficulty" ];
|
|
}
|
|
|
|
return attackerValue;
|
|
}
|
|
|
|
alien_noncombat()
|
|
{
|
|
self ScrAgentSetGoalPos( self.origin );
|
|
while ( 1 )
|
|
{
|
|
if ( IsAlive( self.enemy ) )
|
|
{
|
|
break;
|
|
}
|
|
//iprintln( "Alien non-combat" );
|
|
wait 1;
|
|
}
|
|
}
|
|
|
|
// follows its owner if owner is alive, else find another owner, if no one is alive, it stays put
|
|
alien_pet_follow()
|
|
{
|
|
follow_dist = 768;
|
|
if ( isdefined( self.petFollowDist ) )
|
|
follow_dist = self.petFollowDist;
|
|
|
|
follow_dist_sqr = follow_dist * follow_dist;
|
|
|
|
self ScrAgentSetGoalRadius( follow_dist );
|
|
|
|
while ( !IsAlive( self.enemy ) )
|
|
{
|
|
if ( !isdefined( self.owner ) || !IsAlive( self.owner ) )
|
|
{
|
|
//find new owner
|
|
alive_player = undefined;
|
|
foreach ( player in level.players )
|
|
{
|
|
if ( isalive( player ) )
|
|
{
|
|
alive_player = player;
|
|
break;
|
|
}
|
|
}
|
|
if ( isdefined( alive_player ) )
|
|
{
|
|
self.owner = alive_player;
|
|
}
|
|
else
|
|
{
|
|
self ScrAgentSetGoalPos( self.origin );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
current_dist_sqr = DistanceSquared( self.owner.origin, self.origin );
|
|
if ( current_dist_sqr > follow_dist_sqr )
|
|
{
|
|
target_node = self get_pet_follow_node( self.owner );
|
|
if ( !isDefined( target_node ) )
|
|
{
|
|
wait 1.0;
|
|
continue;
|
|
}
|
|
self ScrAgentSetGoalNode( target_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
wait RandomFloatRange( 0.5, 1.5 );
|
|
}
|
|
}
|
|
wait 0.05;
|
|
}
|
|
}
|
|
|
|
alien_wait_for_combat( enemy )
|
|
{
|
|
self endon ( "go to combat" );
|
|
|
|
/# debug_alien_ai_state( "wait_for_combat" ); #/
|
|
self thread monitor_proximity_with_enemy( enemy );
|
|
self thread watch_for_damage( enemy );
|
|
self thread random_movemode_change( 0, 70, 30, 1.5, 3.0 );
|
|
|
|
while ( 1 )
|
|
{
|
|
should_go_left = cointoss();
|
|
|
|
nodes = GetNodesInRadius( self.origin, 512, 256, 256 );
|
|
|
|
if ( nodes.size == 0 )
|
|
{
|
|
wait 1; // Player is probably in UFO, don't spam wait for combat
|
|
self notify( "go to combat" );
|
|
return;
|
|
}
|
|
|
|
enemy_to_alien_vector = self.origin - enemy.origin;
|
|
enemy_to_alien_angle = VectorToAngles ( enemy_to_alien_vector );
|
|
override_vector = AnglesToRight ( enemy_to_alien_angle );
|
|
if ( should_go_left )
|
|
{
|
|
override_vector = override_vector * -1;
|
|
}
|
|
filters = [];
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_weight" ] = 2.0;
|
|
filters[ "min_height" ] = -32.0;
|
|
filters[ "max_height" ] = 128.0;
|
|
filters[ "height_weight" ] = 1.0;
|
|
filters[ "enemy_los" ] = true;
|
|
filters[ "enemy_los_weight" ] = 2.0;
|
|
filters[ "min_dist_from_enemy" ] = 600.0;
|
|
filters[ "max_dist_from_enemy" ] = 1000.0;
|
|
filters[ "desired_dist_from_enemy" ] = 800.0;
|
|
filters[ "dist_from_enemy_weight" ] = 3.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 400.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 1.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 1.0;
|
|
filters[ "direction_override" ] = override_vector;
|
|
|
|
node_to_go = get_retreat_node_rated( enemy, filters, nodes );
|
|
self ScrAgentSetGoalNode( node_to_go );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
wait RandomFloatRange( 1.5, 3.5 );
|
|
|
|
if ( should_i_attack( enemy ) )
|
|
{
|
|
self notify( "go to combat" );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
alien_retreat( enemy )
|
|
{
|
|
self notify ( "start retreat" );
|
|
self set_alien_movemode( "run" );
|
|
|
|
retreat_type = "elevated_delay";
|
|
retreat_direction = "alien_forward";
|
|
|
|
if ( !IsDefined( retreat_type ) || retreat_type == "" )
|
|
retreat_type = "randomize";
|
|
if ( !IsDefined( retreat_direction ) || retreat_direction == "" )
|
|
retreat_direction = "alien_forward";
|
|
|
|
if ( retreat_type == "randomize" )
|
|
{
|
|
random_array = [ "elevated_delay", "cover", "cover", "cover" ];
|
|
choice = randomint( 4 );
|
|
retreat_type = random_array[choice];
|
|
//iprintln( "Retreat: " + retreat_type );
|
|
}
|
|
|
|
switch (retreat_type)
|
|
{
|
|
case "elevated_delay":
|
|
elevated_delay_retreat( enemy, retreat_direction );
|
|
break;
|
|
case "cover":
|
|
cover_retreat( enemy, retreat_direction );
|
|
break;
|
|
default:
|
|
/# iprintln( "Invalid alien_retreat_type: " + retreat_type ); #/
|
|
wait 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
monitor_proximity_with_enemy( enemy )
|
|
{
|
|
self endon( "go to combat" );
|
|
self endon( "damage" );
|
|
self endon( "death" );
|
|
self endon( "alien_main_loop_restart" );
|
|
enemy endon( "death" );
|
|
enemy endon( "disconnect" );
|
|
|
|
wait_for_proximity_check_activation( enemy );
|
|
|
|
while ( 1 )
|
|
{
|
|
if ( DistanceSquared( self.origin, enemy.origin ) < ALIEN_DISTANCE_DO_DAMAGE_WHEN_RETREAT * ALIEN_DISTANCE_DO_DAMAGE_WHEN_RETREAT )
|
|
{
|
|
self.attack_sequence_num = 1;
|
|
self.attack_num = 1;
|
|
self.bypass_max_attacker_counter = true;
|
|
self notify ( "go to combat" );
|
|
break;
|
|
}
|
|
waitframe();
|
|
}
|
|
}
|
|
|
|
watch_for_damage( enemy )
|
|
{
|
|
self endon( "go to combat" );
|
|
self endon( "death" );
|
|
self endon( "alien_main_loop_restart" );
|
|
enemy endon( "death" );
|
|
|
|
self waittill( "damage" );
|
|
|
|
self.bypass_max_attacker_counter = true;
|
|
self notify ( "go to combat" );
|
|
}
|
|
|
|
wait_for_proximity_check_activation( enemy )
|
|
{
|
|
time_out_sec = 2;
|
|
current_time = getTime();
|
|
|
|
while ( 1 )
|
|
{
|
|
if ( DistanceSquared( self.origin, enemy.origin ) > ALIEN_PROXIMITY_CHECK_ACTIVATION_DISTANCE * ALIEN_PROXIMITY_CHECK_ACTIVATION_DISTANCE )
|
|
{
|
|
break;
|
|
}
|
|
else if ( ( getTime() - current_time ) > time_out_sec * 1000 )
|
|
{
|
|
break;
|
|
}
|
|
waitframe();
|
|
}
|
|
}
|
|
|
|
elevated_delay_retreat( enemy, direction )
|
|
{
|
|
/# debug_alien_ai_state( "elevated_delay_retreat" ); #/
|
|
elevated_jump_node = self get_elevated_jump_node ( enemy, direction );
|
|
if ( !isDefined( elevated_jump_node ))
|
|
{
|
|
wait 0.05;
|
|
return;
|
|
}
|
|
|
|
self ScrAgentSetGoalNode( elevated_jump_node );
|
|
self ScrAgentSetGoalRadius( 32 );
|
|
self waittill( "goal_reached" );
|
|
wait 1.5;
|
|
}
|
|
|
|
get_elevated_jump_node ( enemy, direction )
|
|
{
|
|
jump_nodes = GetNodesInRadius( enemy.origin, 800, 400, 256, "jump" );
|
|
if ( jump_nodes.size == 0 )
|
|
{
|
|
return; // Retreat failed
|
|
}
|
|
|
|
filters = [];
|
|
if ( GetDvarInt( "alien_retreat_towards_spawn") == 1 )
|
|
{
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_override" ] = VectorNormalize( self.spawnOrigin - enemy.origin );
|
|
filters[ "direction_weight" ] = 8.0;
|
|
}
|
|
else
|
|
{
|
|
filters[ "direction" ] = direction;
|
|
filters[ "direction_weight" ] = 1.0;
|
|
}
|
|
|
|
filters[ "min_height" ] = 64.0;
|
|
filters[ "max_height" ] = 400.0;
|
|
filters[ "height_weight" ] = 2.0;
|
|
filters[ "enemy_los" ] = true;
|
|
filters[ "enemy_los_weight" ] = 2.0;
|
|
filters[ "min_dist_from_enemy" ] = 400.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
filters[ "desired_dist_from_enemy" ] = 600.0;
|
|
filters[ "dist_from_enemy_weight" ] = 3.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 200.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 1.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 1.0;
|
|
|
|
result = get_retreat_node_rated( enemy, filters, jump_nodes );
|
|
|
|
return result;
|
|
}
|
|
|
|
cover_retreat( enemy, direction )
|
|
{
|
|
EXTRA_COVER_NODE_AFTER_1ST_COVER = 1;
|
|
|
|
/# debug_alien_ai_state( "cover_retreat" ); #/
|
|
self endon ( "go to combat" );
|
|
|
|
self thread monitor_proximity_with_enemy( enemy );
|
|
self thread watch_for_damage( enemy );
|
|
|
|
filters = [];
|
|
if ( GetDvarInt("alien_cover_node_retreat" ) == 1 )
|
|
{
|
|
filters[ "direction" ] = "cover";
|
|
filters[ "direction_weight" ] = 2.0;
|
|
filters[ "enemy_los_weight" ] = 0.0;
|
|
filters[ "max_dist_from_enemy" ] = 1200.0;
|
|
}
|
|
else if ( GetDvarInt( "alien_retreat_towards_spawn" ) == 1 )
|
|
{
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_override" ] = VectorNormalize( self.spawnOrigin - enemy.origin );
|
|
filters[ "direction_weight" ] = 8.0;
|
|
filters[ "enemy_los_weight" ] = 6.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
}
|
|
else
|
|
{
|
|
filters[ "direction" ] = direction;
|
|
filters[ "direction_weight" ] = 1.0;
|
|
filters[ "enemy_los_weight" ] = 6.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
}
|
|
|
|
filters[ "min_height" ] = -32.0;
|
|
filters[ "max_height" ] = 128.0;
|
|
filters[ "height_weight" ] = 1.0;
|
|
filters[ "enemy_los" ] = false;
|
|
filters[ "min_dist_from_enemy" ] = 400.0;
|
|
filters[ "desired_dist_from_enemy" ] = 600.0;
|
|
filters[ "dist_from_enemy_weight" ] = 2.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 200.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 1.0;
|
|
filters[ "min_dist_from_current_position" ] = 256.0;
|
|
filters[ "min_dist_from_current_position_weight" ] = 3.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 1.0;
|
|
|
|
cover_node = self get_cover_node( enemy, filters );
|
|
if ( !isDefined( cover_node ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
self ScrAgentSetGoalNode( cover_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
|
|
for( i = 0; i < EXTRA_COVER_NODE_AFTER_1ST_COVER; i++ )
|
|
{
|
|
filters[ "direction" ] = "alien_forward";
|
|
cover_node = self get_cover_node( enemy, filters );
|
|
if ( !isDefined( cover_node ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
self ScrAgentSetGoalNode( cover_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
}
|
|
|
|
get_cover_node ( enemy, filters )
|
|
{
|
|
if ( GetDvarInt("alien_cover_node_retreat" ) == 1 )
|
|
nodes = GetNodesInRadius( enemy.origin, 800, 400, 256, "cover stand" );
|
|
else
|
|
nodes = GetNodesInRadius( enemy.origin, 800, 400, 256 );
|
|
|
|
if ( nodes.size == 0 )
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
result = get_retreat_node_rated( enemy, filters, nodes );
|
|
|
|
return result;
|
|
}
|
|
|
|
circling_retreat( enemy, direction )
|
|
{
|
|
/# debug_alien_ai_state( "circling_retreat" ); #/
|
|
self endon ( "go to combat" );
|
|
|
|
self thread monitor_proximity_with_enemy( enemy );
|
|
self thread watch_for_damage( enemy );
|
|
|
|
//Calculate the 1st node to go to after melee
|
|
nodes = GetNodesInRadius( enemy.origin, 800, 400, 256 );
|
|
|
|
if ( nodes.size == 0 )
|
|
{
|
|
/#alien_path_error( "Unable to find any node within 150-unit to 800-unit radius. Talk to level designer." );#/
|
|
nodes = GetNodesInRadius( enemy.origin, 2000, 400, 256 );
|
|
}
|
|
|
|
filters = [];
|
|
filters[ "direction" ] = direction;
|
|
filters[ "direction_weight" ] = 1.0;
|
|
filters[ "min_height" ] = -32.0;
|
|
filters[ "max_height" ] = 32.0;
|
|
filters[ "height_weight" ] = 2.0;
|
|
filters[ "enemy_los" ] = true;
|
|
filters[ "enemy_los_weight" ] = 2.0;
|
|
filters[ "min_dist_from_enemy" ] = 400.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
filters[ "desired_dist_from_enemy" ] = 600.0;
|
|
filters[ "dist_from_enemy_weight" ] = 3.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 200.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 1.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 2.0;
|
|
|
|
first_circle_node = get_retreat_node_rated( enemy, filters, nodes );
|
|
|
|
self ScrAgentSetGoalNode( first_circle_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
|
|
should_go_left = RandomInt ( 2 );
|
|
for ( i = 0; i < ( GetDvarInt ( "alien_circling_retreat_num_nodes" ) - 1 ); i++ )
|
|
{
|
|
enemy_to_alien_vector = self.origin - enemy.origin;
|
|
enemy_to_alien_angle = VectorToAngles ( enemy_to_alien_vector );
|
|
override_vector = AnglesToRight ( enemy_to_alien_angle );
|
|
if ( should_go_left )
|
|
{
|
|
override_vector = override_vector * -1;
|
|
}
|
|
nodes = GetNodesInRadius( enemy.origin, 800, 500, 256 );
|
|
if ( nodes.size == 0 )
|
|
{
|
|
/#alien_path_error( "Unable to find any node within 500-unit to 800-unit radius. Talk to level designer." );#/
|
|
nodes = GetNodesInRadius( enemy.origin, 2000, 400, 256 );
|
|
}
|
|
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_override" ] = override_vector;
|
|
filters[ "direction_weight" ] = 2.0;
|
|
filters[ "random_weight" ] = 1.0;
|
|
filters[ "min_dist_from_current_position" ] = 256.0;
|
|
filters[ "min_dist_from_current_position_weight" ] = 3.0;
|
|
next_circle_node = get_retreat_node_rated( enemy, filters, nodes );
|
|
|
|
self ScrAgentSetGoalNode( next_circle_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
}
|
|
|
|
elevated_circling_retreat( enemy, direction )
|
|
{
|
|
/# debug_alien_ai_state( "elevated_circling_retreat" ); #/
|
|
self endon ( "go to combat" );
|
|
|
|
self thread monitor_proximity_with_enemy( enemy );
|
|
self thread watch_for_damage( enemy );
|
|
|
|
elevated_jump_node = self get_elevated_jump_node ( enemy, direction );
|
|
if ( !isDefined( elevated_jump_node ))
|
|
{
|
|
return;
|
|
}
|
|
|
|
self ScrAgentSetGoalNode( elevated_jump_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
|
|
filters = [];
|
|
filters[ "direction" ] = direction;
|
|
filters[ "direction_weight" ] = 1.0;
|
|
filters[ "min_height" ] = -32.0;
|
|
filters[ "max_height" ] = 32.0;
|
|
filters[ "height_weight" ] = 2.0;
|
|
filters[ "enemy_los" ] = true;
|
|
filters[ "enemy_los_weight" ] = 2.0;
|
|
filters[ "min_dist_from_enemy" ] = 400.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
filters[ "desired_dist_from_enemy" ] = 600.0;
|
|
filters[ "dist_from_enemy_weight" ] = 3.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 200.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 1.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 2.0;
|
|
|
|
should_go_left = RandomInt ( 2 );
|
|
for ( i = 0; i < ( GetDvarInt ( "alien_circling_retreat_num_nodes" ) - 1 ); i++ )
|
|
{
|
|
enemy_to_alien_vector = self.origin - enemy.origin;
|
|
enemy_to_alien_angle = VectorToAngles ( enemy_to_alien_vector );
|
|
override_vector = AnglesToRight ( enemy_to_alien_angle );
|
|
if ( should_go_left )
|
|
{
|
|
override_vector = override_vector * -1;
|
|
}
|
|
|
|
jump_nodes = GetNodesInRadius( self.origin, 800, 250, 256, "jump" );
|
|
if ( jump_nodes.size == 0 )
|
|
{
|
|
return; // This is fine, if there are no jump nodes nearby, don't retreat
|
|
}
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_override" ] = override_vector;
|
|
filters[ "direction_weight" ] = 2.0;
|
|
filters[ "random_weight" ] = 1.0;
|
|
filters[ "min_dist_from_current_position" ] = 256.0;
|
|
filters[ "min_dist_from_current_position_weight" ] = 3.0;
|
|
next_circle_node = get_retreat_node_rated( enemy, filters, jump_nodes );
|
|
|
|
self ScrAgentSetGoalNode( next_circle_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
}
|
|
|
|
close_range_retreat( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "close_range_retreat" ); #/
|
|
self endon ( "go to combat" );
|
|
|
|
self thread monitor_proximity_with_enemy( enemy );
|
|
self thread watch_for_damage( enemy );
|
|
self set_alien_movemode( "run" );
|
|
|
|
MIN_OFFSET_YAW = 55;
|
|
MAX_OFFSET_YAW = 75;
|
|
MIN_OFFSET_DISTANCE = 256;
|
|
MAX_OFFSET_DISTANCE = 256;
|
|
|
|
retreatLocation = get_offset_location_from_enemy( enemy, MIN_OFFSET_DISTANCE, MAX_OFFSET_DISTANCE, MIN_OFFSET_YAW, MAX_OFFSET_YAW );
|
|
|
|
nodes = GetNodesInRadius( retreatLocation, 256, 0, 128, "path" );
|
|
|
|
if ( nodes.size == 0 )
|
|
{
|
|
/#alien_path_error( "Unable to find any node within 300-unit radius. Retreat location: " + retreatLocation );#/
|
|
nodes = GetNodesInRadius( retreatLocation, 500, 256, 128, "path" );
|
|
}
|
|
|
|
if ( nodes.size == 0 )
|
|
{
|
|
wait 0.2;
|
|
return;
|
|
}
|
|
|
|
filters = [];
|
|
if ( GetDvarInt( "alien_retreat_towards_spawn" ) == 1 )
|
|
{
|
|
filters[ "direction_weight" ] = 8.0;
|
|
}
|
|
else
|
|
{
|
|
filters[ "direction_weight" ] = 3.0;
|
|
}
|
|
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_override"] = VectorNormalize( retreatLocation - self.origin );
|
|
filters[ "min_height" ] = -32.0;
|
|
filters[ "max_height" ] = 32.0;
|
|
filters[ "height_weight" ] = 6.0;
|
|
filters[ "enemy_los" ] = false;
|
|
filters[ "enemy_los_weight" ] = 0.0;
|
|
filters[ "min_dist_from_enemy" ] = 150.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
filters[ "desired_dist_from_enemy" ] = 200.0;
|
|
filters[ "dist_from_enemy_weight" ] = 3.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 150.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 1.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 2.0;
|
|
|
|
close_range_node = get_retreat_node_rated( enemy, filters, nodes );
|
|
|
|
self ScrAgentSetGoalNode( close_range_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
|
|
get_retreat_node_rated( enemy, filters, nodes )
|
|
{
|
|
/*
|
|
The "filters" parameter is a table that looks like the one below:
|
|
|
|
filters = [];
|
|
filters[ "direction" ] = "front"; // Valid values are "front", "behind", "alien_forward", "cover" and "override"
|
|
filters[ "direction_override" ] = dir_vector; // Only if direction is "override". Vector.
|
|
filters[ "direction_weight" ] = 1.0; // Max weight of direction
|
|
filters[ "min_height" ] = 72.0; // Min height of nodes
|
|
filters[ "max_height" ] = 128.0; // Max height of nodes
|
|
filters[ "height_weight" ] = 1.0; // Weight of height filter
|
|
filters[ "enemy_los" ] = false; // Whether the enemy should be in LOS or not
|
|
filters[ "enemy_los_weight" ] = 0.0; // Weight of LOS check
|
|
filters[ "min_dist_from_enemy" ] = 100.0; // Min distance from enemy
|
|
filters[ "max_dist_from_enemy" ] = 500.0; // Max distance from enemy
|
|
filters[ "desired_dist_from_enemy" ] = 250.0; // Desired distance from enemy
|
|
filters[ "dist_from_enemy_weight" ] = 2.0; // Weight of distance at desired distance
|
|
filters[ "min_dist_from_all_enemies" ] = 200.0; // Min distance from all enemies (players)
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 1.0; // Weight of min distance to all enemies
|
|
filters[ "min_dist_from_current_position" ] = 128.0; // New node must be this distance from current origin
|
|
filters[ "min_dist_from_current_position_weight" ] = 1.0; // Weight of dist from current origin
|
|
filters[ "not_recently_used_weight" ] = 2.0; // Weight for nodes that are not recently used
|
|
filters[ "recently_used_time_limit" ] = 6.0; // Amount of time a node is considered recently used for
|
|
filters[ "random_weight" ] = 2.0; // Amount of randomness to add
|
|
filters[ "test_offset" ] = (0,0,256.0) // Offset from each node to perform tests at
|
|
*/
|
|
|
|
if ( nodes.size == 0 )
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
direction = filters[ "direction" ];
|
|
direction_weight = filters[ "direction_weight" ];
|
|
min_height = filters[ "min_height" ];
|
|
max_height = filters[ "max_height" ];
|
|
height_weight = filters[ "height_weight" ];
|
|
enemy_los = filters[ "enemy_los" ];
|
|
enemy_los_weight = filters[ "enemy_los_weight" ];
|
|
min_dist_from_enemy = filters[ "min_dist_from_enemy" ];
|
|
max_dist_from_enemy = filters[ "max_dist_from_enemy" ];
|
|
desired_dist_from_enemy = filters[ "desired_dist_from_enemy" ];
|
|
dist_from_enemy_weight = filters[ "dist_from_enemy_weight" ];
|
|
min_dist_from_all_enemies = filters[ "min_dist_from_all_enemies" ];
|
|
min_dist_from_all_enemies_weight = filters[ "min_dist_from_all_enemies_weight" ];
|
|
min_dist_from_current_position = filters[ "min_dist_from_current_position" ];
|
|
min_dist_from_current_position_weight = filters[ "min_dist_from_current_position_weight" ];
|
|
not_recently_used_weight = filters[ "not_recently_used_weight" ];
|
|
random_weight = filters[ "random_weight" ];
|
|
|
|
if ( IsDefined( filters[ "recently_used_time_limit" ] ) )
|
|
recently_used_time_limit = filters[ "recently_used_time_limit" ];
|
|
else
|
|
recently_used_time_limit = RECENT_TIME_LIMIT;
|
|
|
|
if ( IsDefined( filters[ "test_offset" ] ) )
|
|
test_offset = filters[ "test_offset" ];
|
|
else
|
|
test_offset = ( 0.0, 0.0, 0.0 );
|
|
|
|
enemy_forward_vector = undefined;
|
|
if( isPlayer( enemy ))
|
|
{
|
|
enemy_forward_vector = AnglesToForward ( enemy GetPlayerAngles() );
|
|
}
|
|
else if ( IsDefined( enemy ) )
|
|
{
|
|
enemy_forward_vector = AnglesToForward ( enemy.angles );
|
|
}
|
|
target_vector = undefined;
|
|
use_cover_target_vector = false;
|
|
if ( direction == "player_front" )
|
|
{
|
|
target_vector = enemy_forward_vector;
|
|
}
|
|
else if ( direction == "player_behind" )
|
|
{
|
|
target_vector = enemy_forward_vector * -1;
|
|
}
|
|
else if ( direction == "alien_forward" )
|
|
{
|
|
target_vector = AnglesToForward ( self.angles );
|
|
}
|
|
else if ( direction == "alien_backward" )
|
|
{
|
|
target_vector = AnglesToForward ( self.angles ) * -1;
|
|
}
|
|
else if ( direction == "cover" )
|
|
{
|
|
use_cover_target_vector = true;
|
|
}
|
|
else if ( direction == "override" )
|
|
{
|
|
assert( isDefined( filters[ "direction_override" ] ) );
|
|
target_vector = filters[ "direction_override" ];
|
|
}
|
|
else
|
|
{
|
|
AssertMsg ( "Invalid direction. It should be 'front', 'behind', or 'alien forward'" );
|
|
}
|
|
|
|
index = 0;
|
|
best_node_rating = -1.0;
|
|
best_index = 0;
|
|
wait_count = 0;
|
|
// num_traces = 0;
|
|
while ( index < nodes.size )
|
|
{
|
|
node = nodes[ index ];
|
|
node_origin = node.origin + test_offset;
|
|
rating = 0.0;
|
|
|
|
//filter the nodes based on the min_height requirements
|
|
if ( height_Weight > 0.0 )
|
|
{
|
|
if (( node_origin[2] - enemy.origin[2] ) > min_height
|
|
&& ( node_origin[2] - enemy.origin[2] ) < max_height )
|
|
{
|
|
rating += height_weight;
|
|
}
|
|
}
|
|
|
|
// Direction
|
|
compare_vector = undefined;
|
|
if ( use_cover_target_vector )
|
|
{
|
|
target_vector = AnglesToForward( node.angles );
|
|
compare_vector = VectorNormalize( enemy.origin - node_origin );
|
|
}
|
|
else
|
|
{
|
|
compare_vector = VectorNormalize ( node_origin - self.origin );
|
|
|
|
}
|
|
|
|
dot = VectorDot( target_vector, compare_vector );
|
|
rating += (dot + 1.0) * 0.5 * direction_weight;
|
|
|
|
// Dist from enemy
|
|
if ( dist_from_enemy_weight > 0.0 )
|
|
{
|
|
dist = distance( node_origin, enemy.origin );
|
|
if ( dist > min_dist_from_enemy && dist < max_dist_from_enemy )
|
|
{
|
|
dist_rating = 0.0;
|
|
if ( dist > desired_dist_from_enemy )
|
|
{
|
|
dist_rating += 1 - ( ( dist - desired_dist_from_enemy ) / ( max_dist_from_enemy - desired_dist_from_enemy ) );
|
|
}
|
|
else
|
|
{
|
|
dist_rating += ( dist - min_dist_from_enemy ) / ( desired_dist_from_enemy - min_dist_from_enemy );
|
|
}
|
|
rating += dist_rating * dist_from_enemy_weight;
|
|
}
|
|
}
|
|
|
|
// Dist from all enemies
|
|
if ( min_dist_from_all_enemies_weight )
|
|
{
|
|
min_dist_success = true;
|
|
foreach ( player in level.players )
|
|
{
|
|
if ( !IsDefined( enemy ) || player != enemy )
|
|
{
|
|
if ( DistanceSquared( node_origin, player.origin ) < min_dist_from_all_enemies * min_dist_from_all_enemies )
|
|
{
|
|
min_dist_success = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( min_dist_success )
|
|
{
|
|
rating += min_dist_from_all_enemies_weight;
|
|
}
|
|
}
|
|
|
|
if ( IsDefined( min_dist_from_current_position_weight ) && min_dist_from_current_position_weight > 0.0 )
|
|
{
|
|
if ( DistanceSquared( self.origin, node_origin ) > min_dist_from_current_position*min_dist_from_current_position )
|
|
{
|
|
rating += min_dist_from_current_position_weight;
|
|
}
|
|
}
|
|
|
|
// Not recently used
|
|
if ( !node_recently_used( node, recently_used_time_limit ) )
|
|
{
|
|
rating += not_recently_used_weight;
|
|
}
|
|
|
|
// Random
|
|
if ( random_weight > 0.0 )
|
|
{
|
|
rating += RandomFloat( random_weight );
|
|
}
|
|
|
|
// Enemy LOS
|
|
// Only add LOS rating if the node has a possibility of being the best node.
|
|
if ( enemy_los_weight > 0.0 && rating > best_node_rating - enemy_los_weight )
|
|
{
|
|
while ( max_traces_reached() )
|
|
{
|
|
wait_count++;
|
|
if ( wait_count >= 20 )
|
|
{
|
|
break;
|
|
}
|
|
|
|
waitframe();
|
|
}
|
|
|
|
// This safety check means that part of the node list will be evaluated against a now missing enemy, and the other part
|
|
// will be evaluated without enemy_los_weight. The result is slightly unoptimal node evalution in a corner case. That's fine.
|
|
if ( IsDefined( enemy ) )
|
|
{
|
|
trace_passed = SightTracePassed( enemy get_eye_position(), node_origin + ( 0, 0, 50 ), false, self );
|
|
//num_traces++;
|
|
level.nodeFilterTracesThisFrame++;
|
|
if ( enemy_los == trace_passed )
|
|
{
|
|
rating += enemy_los_weight;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( rating > best_node_rating )
|
|
{
|
|
best_node_rating = rating;
|
|
best_index = index;
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
//AssertEx( wait_count < 20, "Scheduled t races for get_retreat_node_rated took more than 1 second!" );
|
|
// iprintln( "Num traces: " + num_traces );
|
|
|
|
register_node( nodes[best_index] );
|
|
return nodes[best_index];
|
|
|
|
}
|
|
|
|
get_eye_position()
|
|
{
|
|
assert( isDefined( self ) );
|
|
if ( isAlive( self ) && ( isPlayer( self ) || isAgent( self ) ) )
|
|
{
|
|
return self getEye();
|
|
}
|
|
|
|
return self.origin;
|
|
}
|
|
|
|
max_traces_reached()
|
|
{
|
|
MAX_TRACE_COUNT_PER_FRAME = 5;
|
|
if ( gettime() > level.nodeFilterTracesTime )
|
|
{
|
|
level.nodeFilterTracesTime = gettime();
|
|
level.nodeFilterTracesThisFrame = 0;
|
|
}
|
|
|
|
return level.nodeFilterTracesThisFrame >= MAX_TRACE_COUNT_PER_FRAME;
|
|
}
|
|
|
|
default_approach( enemy, attack_counter )
|
|
{
|
|
/# debug_alien_ai_state( "default_approach" ); #/
|
|
/# debug_alien_attacker_state( "attacking" ); #/
|
|
|
|
approach_node = do_initial_approach( enemy );
|
|
|
|
/*
|
|
if ( !in_front_of( enemy ))
|
|
self thread sneak_up_on( enemy );
|
|
*/
|
|
|
|
swipe_chance = self.swipeChance;
|
|
should_swipe = ( RandomFloat( 1.0 ) < swipe_chance );
|
|
|
|
if ( !should_swipe && self go_to_leaping_melee_position( enemy ) )
|
|
{
|
|
|
|
// Trigger the melee
|
|
/*self.wall_leap_melee_node = find_wall_leap_node( enemy );
|
|
if ( isDefined( self.wall_leap_melee_node ) )
|
|
{
|
|
return "wall";
|
|
}*/
|
|
|
|
return "leap";
|
|
}
|
|
|
|
// We failed the leaping melee, try the swipe
|
|
return go_for_swipe( enemy, approach_node );
|
|
}
|
|
|
|
do_initial_approach( enemy )
|
|
{
|
|
self thread jogWhenCloseToEnemy( enemy );
|
|
|
|
// Approach a node near enemy if farther than ALIEN_APPROACH_DIST_SQR
|
|
approach_node = undefined;
|
|
if ( DistanceSquared( self.origin, enemy.origin ) > ALIEN_APPROACH_DIST_SQR )
|
|
approach_node = approach_enemy( ALIEN_LEAP_MELEE_DISTANCE_MAX, enemy, 3 );
|
|
|
|
return approach_node;
|
|
}
|
|
|
|
jogWhenCloseToEnemy( enemy )
|
|
{
|
|
self notify( "jogWhenCloseToEnemy" );
|
|
self endon( "jogWhenCloseToEnemy" );
|
|
self endon( "death" );
|
|
enemy endon( "death" );
|
|
enemy endon( "disconnect" );
|
|
level endon( "game_ended" );
|
|
|
|
if ( distanceSquared( self.origin, enemy.origin ) >= ALIEN_JOG_CHANGE_DISTANCE * ALIEN_JOG_CHANGE_DISTANCE )
|
|
self set_alien_movemode( "run" );
|
|
|
|
while ( true )
|
|
{
|
|
wait ( 1.0 );
|
|
|
|
if ( distanceSquared ( self.origin, enemy.origin ) < ALIEN_JOG_CHANGE_DISTANCE * ALIEN_JOG_CHANGE_DISTANCE )
|
|
break;
|
|
}
|
|
switchToJog();
|
|
}
|
|
|
|
switchToJog()
|
|
{
|
|
// If the agent is already in jog, dont reduce speed again [IWSIX-173775]
|
|
if( self.movemode == "jog" )
|
|
return;
|
|
|
|
SLOWER_PLAYBACK_RATE = 0.9;
|
|
|
|
self set_alien_movemode( "jog" );
|
|
self.moveplaybackrate *= SLOWER_PLAYBACK_RATE;
|
|
self thread backToRunOnDamage( true );
|
|
}
|
|
|
|
backToRunOnDamage( whizby )
|
|
{
|
|
self notify( "backToRunOnDamage" );
|
|
self endon( "backToRunOnDamage" );
|
|
self endon( "death" );
|
|
level endon( "game_ended" );
|
|
|
|
if ( isdefined( whizby ) && whizby )
|
|
self waittill_any( "damage", "start_attack", "bulletwhizby" );
|
|
else
|
|
self waittill_any( "damage", "start_attack" );
|
|
|
|
self set_alien_movemode( "run" );
|
|
self.moveplaybackrate = self.defaultmoveplaybackrate;
|
|
}
|
|
|
|
sneak_up_on( enemy )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "start_attack" );
|
|
|
|
self set_alien_movemode( "walk" );
|
|
ENEMY_GET_AWAY_SCALER = 1.05;
|
|
|
|
while( true )
|
|
{
|
|
wait( 1.0 );
|
|
|
|
if( in_front_players() //in front of any player
|
|
|| distanceSquared( self.origin, enemy.origin ) > ALIEN_LEAP_MELEE_DISTANCE_MAX * ALIEN_LEAP_MELEE_DISTANCE_MAX * ENEMY_GET_AWAY_SCALER ) //Enemy is getting away
|
|
{
|
|
self set_alien_movemode( "run" );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
in_front_players()
|
|
{
|
|
foreach( player in level.players )
|
|
{
|
|
if ( in_front_of( player ))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
in_front_of( enemy )
|
|
{
|
|
CONE_LIMIT = 0.5; //cos( 60 )
|
|
|
|
enemy_to_self = self.origin - enemy.origin;
|
|
enemy_forward = anglesToForward( enemy.angles );
|
|
dot_product = VectorDot( enemy_to_self, enemy_forward );
|
|
|
|
if( dot_product < 0 )
|
|
return false;
|
|
|
|
inside_front_cone = dot_product > CONE_LIMIT;
|
|
return inside_front_cone;
|
|
}
|
|
|
|
go_for_swipe( enemy, approach_node )
|
|
{
|
|
melee_distance = GetDvarInt( "alien_melee_distance" );
|
|
if ( melee_distance < ALIEN_MIN_MELEE_DISTANCE )
|
|
melee_distance = ALIEN_MIN_MELEE_DISTANCE;
|
|
|
|
self run_near_enemy( melee_distance, enemy );
|
|
if ( !self AgentCanSeeSentient( enemy ) )
|
|
return "none";
|
|
|
|
return "swipe";
|
|
}
|
|
|
|
find_wall_leap_node( enemy )
|
|
{
|
|
dist_to_enemy = Distance( self.origin, enemy.origin );
|
|
if ( dist_to_enemy < 200 )
|
|
{
|
|
// too close
|
|
return;
|
|
}
|
|
|
|
// Center nodes to check around center point
|
|
vec_to_enemy = enemy.origin - self.origin;
|
|
center_point = self.origin + ( vec_to_enemy * 0.5 );
|
|
|
|
enemy_eye = enemy get_eye_position();
|
|
COS_MAX_VERTICAL_ANGLE = 0.95757; // 16.75 degrees
|
|
|
|
jump_nodes = GetNodesInRadius( center_point, dist_to_enemy + 100.0, 100.0, 256.0, "node_jump" );
|
|
//println( "Found " + jump_nodes.size + " jump nodes in range." );
|
|
|
|
qualified_nodes = [];
|
|
|
|
foreach ( jump_node in jump_nodes )
|
|
{
|
|
// Must be above the enemy
|
|
if ( jump_node.origin[2] < enemy.origin[2] + 32.0 )
|
|
{
|
|
//line( enemy.origin, jump_node.origin, (0,0,1), 1, false, 60 );
|
|
continue;
|
|
}
|
|
|
|
// Must be in between the player and the alien
|
|
enemy_to_node = enemy.origin - jump_node.origin;
|
|
enemy_to_alien = self.origin - enemy.origin;
|
|
if ( VectorDot( enemy_to_alien, enemy_to_node ) > 0 )
|
|
{
|
|
//line( jump_node.origin, jump_node.origin + ( 0,0,100 ), (1,0,0), 1, false, 60 );
|
|
continue;
|
|
}
|
|
|
|
alien_to_node = self.origin - jump_node.origin;
|
|
alien_to_enemy = enemy_to_alien * -1;
|
|
if ( VectorDot( alien_to_enemy, alien_to_node ) > 0 )
|
|
{
|
|
//line( jump_node.origin, jump_node.origin + ( 0,0,100 ), (0,1,0), 1, false, 60 );
|
|
continue;
|
|
}
|
|
|
|
// Must be far enough away from alien
|
|
if ( DistanceSquared( self.origin, jump_node.origin ) < 256.0 * 256.0 )
|
|
{
|
|
//line( self.origin, jump_node.origin, (1,1,0), 1, false, 60 );
|
|
continue;
|
|
}
|
|
|
|
// Must be far enough away from player
|
|
if ( DistanceSquared( enemy.origin, jump_node.origin ) < 256.0 * 256.0 )
|
|
{
|
|
//line( enemy.origin, jump_node.origin, (1,1,0), 1, false, 60 );
|
|
continue;
|
|
}
|
|
|
|
// Must be within JUMP_MAX_VERTICAL_ANGLE of enemy's eye height
|
|
enemy_eye_to_node = VectorNormalize( jump_node.origin - enemy_eye );
|
|
enemy_eye_to_node_no_z = VectorNormalize( enemy_eye_to_node * ( 1,1,0 ) );
|
|
dot = VectorDot( enemy_eye_to_node, enemy_eye_to_node_no_z );
|
|
if ( dot < COS_MAX_VERTICAL_ANGLE )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Must have line of sight to alien
|
|
alien_eye = self GetEye();
|
|
jump_node_offset = jump_node.origin + (0,0,32);
|
|
trace_result = BulletTrace( alien_eye, jump_node_offset , false, self );
|
|
if ( trace_result[ "surfacetype" ] != "none" )
|
|
{
|
|
//line( self.origin, jump_node_offset, (1,0,0), 1, false, 60 );
|
|
continue;
|
|
}
|
|
//line( self.origin, jump_node_offset, (0,1,0), 1, false, 60 );
|
|
|
|
// Must have line of sight to enemy
|
|
enemy_eye = enemy get_eye_position();
|
|
trace_result = BulletTrace( jump_node_offset, enemy_eye, false, enemy );
|
|
if ( trace_result[ "surfacetype" ] != "none" )
|
|
{
|
|
//line( enemy_eye, jump_node_offset, (1,0,0), 1, false, 60 );
|
|
continue;
|
|
}
|
|
//line( enemy_eye, jump_node_offset, (0,1,0), 1, false, 60 );
|
|
|
|
qualified_nodes [ qualified_nodes.size ] = jump_node;
|
|
}
|
|
|
|
if ( qualified_nodes.size == 0 )
|
|
{
|
|
return undefined;
|
|
}
|
|
else
|
|
{
|
|
filters = [];
|
|
filters[ "direction" ] = "alien_forward";
|
|
filters[ "direction_weight" ] = 1.0;
|
|
filters[ "height_weight" ] = 0.0;
|
|
filters[ "enemy_los_weight" ] = 0.0;
|
|
filters[ "dist_from_enemy_weight" ] = 0.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 0.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 1.0;
|
|
wall_leap_node = get_retreat_node_rated( enemy, filters, qualified_nodes );
|
|
//line( wall_leap_node.origin, wall_leap_node.origin + ( 0,0,100 ), (0,1,1), 1, false, 60 );
|
|
return wall_leap_node;
|
|
}
|
|
}
|
|
|
|
swipe_melee( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "swipe_melee" ); #/
|
|
self.melee_type = "swipe";
|
|
alien_melee( enemy );
|
|
}
|
|
|
|
leap_melee( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "leap_melee" ); #/
|
|
self.melee_type = "leap";
|
|
alien_melee( enemy );
|
|
}
|
|
|
|
wall_leap_melee( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "wall_leap_melee" ); #/
|
|
self.melee_type = "wall";
|
|
assert( IsDefined( self.wall_leap_melee_node ) );
|
|
alien_melee( enemy );
|
|
}
|
|
|
|
synch_melee( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "synch_melee" ); #/
|
|
self.melee_type = "synch";
|
|
alien_melee( enemy );
|
|
}
|
|
|
|
badpath_jump( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "badpath_jump" ); #/
|
|
self.melee_type = "badpath_jump";
|
|
alien_melee( enemy );
|
|
}
|
|
|
|
swipe_static_melee( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "swipe_static_melee" ); #/
|
|
self.melee_type = "swipe_static";
|
|
alien_melee( enemy );
|
|
}
|
|
|
|
alien_melee( enemy )
|
|
{
|
|
// Effectively clear our path before meleeing
|
|
if ( melee_okay() && self AttemptMelee() )
|
|
{
|
|
if ( self get_alien_type() != "spitter" && self get_alien_type() != "seeder" )
|
|
{
|
|
self ScrAgentSetGoalEntity( enemy );
|
|
self ScrAgentSetGoalRadius( 4096.0 );
|
|
}
|
|
self waittill( "melee_complete" );
|
|
}
|
|
else
|
|
{
|
|
wait 0.2;
|
|
}
|
|
}
|
|
|
|
watch_for_scripted()
|
|
{
|
|
self endon( "death" );
|
|
|
|
while ( 1 )
|
|
{
|
|
if ( IsDefined( self.alien_scripted ) && self.alien_scripted == true )
|
|
{
|
|
self notify( "alien_main_loop_restart" );
|
|
while ( IsDefined( self.alien_scripted ) && self.alien_scripted == true )
|
|
{
|
|
wait 0.05;
|
|
}
|
|
}
|
|
wait 0.05;
|
|
}
|
|
}
|
|
|
|
watch_for_badpath()
|
|
{
|
|
self endon( "death" );
|
|
|
|
self.badpath = false;
|
|
while ( 1 )
|
|
{
|
|
self waittill( "bad_path", start_pos, end_pos );
|
|
//line( start_pos, end_pos, (1,0,0), 1, 0, 20 );
|
|
self.badpath = true;
|
|
|
|
if ( !IsDefined( self.badpathcount ) || ( IsDefined( self.badpathtime ) && gettime() > self.badpathtime + 2000 ))
|
|
{
|
|
self.badpathcount = 0;
|
|
}
|
|
|
|
self.badpathtime = gettime();
|
|
self.badpathcount++;
|
|
self notify( "alien_main_loop_restart" );
|
|
wait 0.05;
|
|
}
|
|
}
|
|
|
|
set_alien_movemode( mode )
|
|
{
|
|
switch( mode )
|
|
{
|
|
case "run":
|
|
case "walk":
|
|
case "jog":
|
|
self.movemode = mode;
|
|
break;
|
|
default:
|
|
AssertMsg( "Invalid alien movemode: " + mode );
|
|
break;
|
|
}
|
|
}
|
|
|
|
watch_for_insolid()
|
|
{
|
|
self endon( "death" );
|
|
if ( self.insolid )
|
|
{
|
|
self HandleInSolid();
|
|
}
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "insolid" );
|
|
self HandleInSolid();
|
|
}
|
|
}
|
|
|
|
HandleInSolid()
|
|
{
|
|
while ( self.insolid )
|
|
{
|
|
wait 0.2;
|
|
}
|
|
|
|
self ScrAgentSetGoalPos( self.origin );
|
|
self notify( "alien_main_loop_restart" );
|
|
}
|
|
|
|
approach_enemy( max_distance, enemy, maxNodeTries )
|
|
{
|
|
if ( max_distance < 32 )
|
|
max_distance = 32;
|
|
|
|
approachNode = get_approach_node( enemy );
|
|
maxDistanceSq = max_distance * max_distance;
|
|
nodeTries = 0;
|
|
|
|
while ( IsDefined( approachNode ) && nodeTries < maxNodeTries)
|
|
{
|
|
if ( run_to_approach_node( approachNode, max_distance, enemy ) )
|
|
{
|
|
return approachNode;
|
|
}
|
|
|
|
if ( DistanceSquared( self.origin, enemy.origin ) < ALIEN_APPROACH_DIST_SQR )
|
|
break;
|
|
|
|
nodeTries++;
|
|
approachNode = get_approach_node( enemy );
|
|
}
|
|
|
|
run_to_enemy( max_distance, enemy );
|
|
}
|
|
|
|
run_to_approach_node( approach_node, max_distance, enemy )
|
|
{
|
|
self notify( "approach_goal_invalid" );
|
|
|
|
self ScrAgentSetGoalRadius( ALIEN_NODE_GOAL_RADIUS );
|
|
self ScrAgentSetGoalNode( approach_node );
|
|
|
|
wait_till_reached_goal_or_distance_from_enemy( approach_node, max_distance, enemy );
|
|
|
|
return DistanceSquared( enemy.origin, self.origin) <= max_distance * max_distance;
|
|
}
|
|
|
|
run_to_enemy( max_distance, enemy )
|
|
{
|
|
self notify( "approach_goal_invalid" );
|
|
|
|
goal_radius = max( max_distance + ALIEN_GOAL_RADIUS_ADJUSTMENT, 32 );
|
|
self ScrAgentSetGoalRadius( goal_radius );
|
|
self ScrAgentSetGoalEntity( enemy );
|
|
|
|
wait_till_distance_from_enemy( max_distance, enemy );
|
|
}
|
|
|
|
wait_till_reached_goal_or_distance_from_enemy( goal_node, max_distance, enemy )
|
|
{
|
|
self endon( "goal_reached" );
|
|
self endon( "approach_goal_invalid" );
|
|
|
|
self thread monitor_approach_goal_invalid( goal_node, enemy );
|
|
wait_till_distance_from_enemy( max_distance, enemy );
|
|
}
|
|
|
|
monitor_approach_goal_invalid( goal_node, enemy )
|
|
{
|
|
self endon( "goal_reached" );
|
|
self endon( "death" );
|
|
self endon( "approach_goal_invalid" );
|
|
enemy endon( "death" );
|
|
|
|
DISTANCE_TOLERANCE_SQ = 450 * 450;
|
|
|
|
while ( IsDefined( goal_node ) && IsDefined( enemy ) )
|
|
{
|
|
if ( DistanceSquared( goal_node.origin, enemy.origin ) > DISTANCE_TOLERANCE_SQ )
|
|
break;
|
|
|
|
enemyToSelf = VectorNormalize( self.origin - enemy.origin );
|
|
enemyToGoal = VectorNormalize( goal_node.origin - enemy.origin );
|
|
|
|
if ( VectorDot( enemytoSelf, enemyToGoal ) < 0 )
|
|
break;
|
|
|
|
wait 0.2;
|
|
}
|
|
|
|
self notify( "approach_goal_invalid" );
|
|
}
|
|
|
|
wait_till_distance_from_enemy( max_distance, enemy )
|
|
{
|
|
max_distance_sqr = max_distance * max_distance;
|
|
|
|
while ( true )
|
|
{
|
|
if ( get_distance_squared_to_enemy( enemy ) < max_distance_sqr )
|
|
{
|
|
break;
|
|
}
|
|
|
|
waitframe();
|
|
}
|
|
}
|
|
|
|
get_distance_squared_to_enemy( enemy )
|
|
{
|
|
if ( IsPlayer( enemy ) && enemy IsOnLadder() )
|
|
return Distance2DSquared( self.origin, enemy.origin );
|
|
|
|
return DistanceSquared( self.origin, enemy.origin );
|
|
}
|
|
|
|
run_near_enemy( max_distance, enemy, approach_node )
|
|
{
|
|
if ( max_distance < 32 )
|
|
{
|
|
max_distance = 32;
|
|
}
|
|
|
|
if ( !IsDefined( approach_node ) || !run_to_approach_node( approach_node, max_distance, enemy ) )
|
|
run_to_enemy( max_distance, enemy );
|
|
}
|
|
|
|
get_offset_location_from_enemy( enemy, minOffsetDistance, maxOffsetDistance, minOffsetYaw, maxOffsetYaw )
|
|
{
|
|
if ( GetDvarInt( "alien_retreat_towards_spawn" ) == 1 )
|
|
targetLocation = self.spawnOrigin;
|
|
else
|
|
targetLocation = self.origin;
|
|
|
|
flatEnemyToTarget = VectorNormalize( ( targetLocation - enemy.origin ) * ( 1, 1, 0 ) );
|
|
yawValue = RandomIntRange( minOffsetYaw, maxOffsetYaw );
|
|
if ( cointoss() )
|
|
yawValue *= -1;
|
|
|
|
rotateAngles = ( 0, yawValue, 0);
|
|
offsetDir = RotateVector( flatEnemyToTarget, rotateAngles );
|
|
offsetDistance = minOffsetDistance;
|
|
if ( minOffsetDistance < maxOffsetDistance )
|
|
offsetDistance = RandomIntRange( minOffsetDistance, maxOffsetDistance );
|
|
|
|
return enemy.origin + offsetDir * offsetDistance;
|
|
}
|
|
|
|
get_approach_node( enemy )
|
|
{
|
|
MIN_OFFSET_YAW = 20;
|
|
MAX_OFFSET_YAW = 30;
|
|
|
|
approachDistance = GetDvarInt( "alien_melee_distance" );
|
|
if ( approachDistance < ALIEN_MIN_MELEE_DISTANCE )
|
|
approachDistance = ALIEN_MIN_MELEE_DISTANCE;
|
|
|
|
approachLocation = get_offset_location_from_enemy( enemy, approachDistance, approachDistance, MIN_OFFSET_YAW, MAX_OFFSET_YAW );
|
|
nodes = GetNodesInRadius( approachLocation, 150, 0, 128, "path" );
|
|
|
|
if ( nodes.size == 0 )
|
|
nodes = GetNodesInRadius( approachLocation, 300, 150, 128, "path" );
|
|
|
|
filters = [];
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_weight" ] = 6.0;
|
|
filters[ "direction_override"] = VectorNormalize( enemy.origin - self.origin );
|
|
filters[ "min_height" ] = -32.0;
|
|
filters[ "max_height" ] = 32.0;
|
|
filters[ "height_weight" ] = 6.0;
|
|
filters[ "enemy_los" ] = false;
|
|
filters[ "enemy_los_weight" ] = 0.0;
|
|
filters[ "min_dist_from_enemy" ] = 150.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
filters[ "desired_dist_from_enemy" ] = Distance( approachLocation, enemy.origin );
|
|
filters[ "dist_from_enemy_weight" ] = 3.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 150.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 1.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 2.0;
|
|
|
|
return get_retreat_node_rated( enemy, filters, nodes );
|
|
}
|
|
|
|
wait_for_valid_leap_melee( enemy )
|
|
{
|
|
random_max_dist = RandomIntRange( -100, 100 ) + ALIEN_LEAP_MELEE_DISTANCE_MAX;
|
|
target_dist_squared = random_max_dist * random_max_dist;
|
|
min_jump_dist_squared = ALIEN_LEAP_MELEE_DISTANCE_MIN * ALIEN_LEAP_MELEE_DISTANCE_MIN;
|
|
|
|
while ( 1 )
|
|
{
|
|
if ( DistanceSquared( self.origin, enemy.origin ) < target_dist_squared )
|
|
{
|
|
break;
|
|
}
|
|
wait 0.05;
|
|
}
|
|
|
|
// We're near the player, start checking whether we can melee
|
|
while ( 1 )
|
|
{
|
|
if ( !isAlive( enemy ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( DistanceSquared( self.origin, enemy.origin ) < min_jump_dist_squared )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( self can_leap_melee( enemy, ALIEN_LEAP_MELEE_DISTANCE_MAX, ALIEN_LEAP_MELEE_DISTANCE_MIN ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
wait 0.05;
|
|
}
|
|
}
|
|
|
|
go_to_leaping_melee_position( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "go_to_leaping_melee_position" ); #/
|
|
self ScrAgentSetGoalEntity( enemy );
|
|
self ScrAgentSetGoalRadius( ALIEN_LEAP_MELEE_DISTANCE_MIN + ALIEN_GOAL_RADIUS_ADJUSTMENT ); // If we don't have line of sight to jump by this point, give up.
|
|
|
|
return wait_for_valid_leap_melee( enemy );
|
|
}
|
|
|
|
go_to_leaping_melee_node( enemy )
|
|
{
|
|
/# debug_alien_ai_state( "go_to_leaping_melee_node (badpath)" ); #/
|
|
nodes = GetNodesInRadius( enemy.origin, 512.0, 200.0, 256.0, "node_jump" );
|
|
valid_nodes = [];
|
|
enemy_eye = enemy get_eye_position();
|
|
node_offset = (0,0,32);
|
|
chosen_node = undefined;
|
|
foreach ( node in nodes )
|
|
{
|
|
// Check validity of leaping attack from here
|
|
trace_result = BulletTracePassed( node.origin + node_offset, enemy_eye, false, self );
|
|
if ( trace_result )
|
|
{
|
|
chosen_node = node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !isDefined( chosen_node ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
self ScrAgentSetGoalNode( chosen_node );
|
|
self ScrAgentSetGoalRadius( ALIEN_LEAP_MELEE_DISTANCE_MIN + ALIEN_GOAL_RADIUS_ADJUSTMENT );
|
|
|
|
target_dist_squared = ALIEN_LEAP_MELEE_DISTANCE_MAX * ALIEN_LEAP_MELEE_DISTANCE_MAX;
|
|
min_jump_dist_squared = 0.0;
|
|
return wait_for_valid_leap_melee( enemy );
|
|
|
|
}
|
|
|
|
can_leap_melee( enemy, max_distance, min_distance )
|
|
{
|
|
if ( ! ( self melee_okay() ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( isDefined( max_distance ))
|
|
{
|
|
if ( DistanceSquared( self.origin, enemy.origin ) > max_distance * max_distance )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
alien_eye = self.origin + (0,0,32);
|
|
enemy_eye = enemy get_eye_position();
|
|
leapEndPos = self maps\mp\agents\alien\_alien_melee::get_melee_position( 1.0, LEAP_OFFSET_XY, enemy );
|
|
self.leapEndPos = leapEndPos;
|
|
|
|
if ( !isDefined( leapEndPos ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If our desired position is too close to our current position, return false
|
|
if ( DistanceSquared( self.origin, leapEndPos ) < min_distance )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Do expensive trajectory check last
|
|
traceResult = TrajectoryCanAttemptAccurateJump( self.origin, AnglesToUp( self.angles ), leapEndPos, AnglesToUp( enemy.angles ), level.alien_jump_melee_gravity, 1.01 * level.alien_jump_melee_speed );
|
|
|
|
return traceResult;
|
|
}
|
|
|
|
melee_okay()
|
|
{
|
|
switch ( self.currentAnimState )
|
|
{
|
|
case "move":
|
|
return true;
|
|
case "melee":
|
|
return true;
|
|
case "idle":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
register_node( node )
|
|
{
|
|
assert( IsDefined( node ) );
|
|
node_info = [];
|
|
node_info [ "node" ] = node;
|
|
node_info [ "time_stamp" ] = getTime();
|
|
level.used_nodes [ level.used_nodes_list_index ] = node_info;
|
|
level.used_nodes_list_index ++;
|
|
if ( level.used_nodes_list_index == level.used_nodes_list_size )
|
|
{
|
|
level.used_nodes_list_index = 0;
|
|
}
|
|
}
|
|
|
|
node_recently_used( node, time_limit )
|
|
{
|
|
// The amount of time after which an used node is considered as "not recently used".
|
|
|
|
result = false;
|
|
for ( i = 0; i < level.used_nodes_list_size; i++ )
|
|
{
|
|
index_scanner = level.used_nodes_list_index - i;
|
|
if ( index_scanner < 0 )
|
|
{
|
|
index_scanner = level.used_nodes_list_size + index_scanner;
|
|
}
|
|
|
|
if ( isDefined ( level.used_nodes [ index_scanner ] ))
|
|
{
|
|
node_info = level.used_nodes [ index_scanner ];
|
|
if (( node == node_info [ "node" ]))
|
|
{
|
|
if (( getTime() - node_info [ "time_stamp" ] ) < time_limit * 1000 )
|
|
{
|
|
result = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
get_attack_sequence_num()
|
|
{
|
|
// We shouldn't attack more than once
|
|
if ( isDefined( self.bypass_max_attacker_counter ) && self.bypass_max_attacker_counter == true )
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if( isDefined( self.attack_sequence_num ) )
|
|
{
|
|
override_value = self.attack_sequence_num;
|
|
self.attack_sequence_num = undefined;
|
|
return override_value;
|
|
}
|
|
|
|
switch( get_alien_type() )
|
|
{
|
|
case "elite":
|
|
case "spitter":
|
|
case "seeder":
|
|
case "locust":
|
|
case "gargoyle":
|
|
return 1;
|
|
default:
|
|
return RandomInt( MAX_ATTACK_SEQUENCES - MIN_ATTACK_SEQUENCES ) + MIN_ATTACK_SEQUENCES;
|
|
}
|
|
}
|
|
|
|
get_attack_num()
|
|
{
|
|
if( isDefined( self.attack_num ) )
|
|
{
|
|
override_value = self.attack_num;
|
|
self.attack_num = undefined;
|
|
return override_value;
|
|
}
|
|
|
|
switch( get_alien_type() )
|
|
{
|
|
case "locust":
|
|
case "gargoyle":
|
|
return 1;
|
|
default:
|
|
return RandomInt( MAX_ATTACKS ) + MIN_ATTACKS;
|
|
}
|
|
}
|
|
|
|
// END =========== Alien cloaking ==============
|
|
|
|
random_movemode_change( run_weight, jog_weight, walk_weight, min_change_time, max_change_time )
|
|
{
|
|
self endon( "go to combat" );
|
|
self endon( "death" );
|
|
|
|
movemode_weight = [];
|
|
movemode_weight[ "run" ] = run_weight;
|
|
movemode_weight[ "jog" ] = jog_weight;
|
|
movemode_weight[ "walk" ] = walk_weight;
|
|
|
|
total_weight = run_weight + jog_weight + walk_weight;
|
|
|
|
min_dist_walk_sqr = 512.0 * 512.0;
|
|
|
|
while ( true )
|
|
{
|
|
|
|
if ( maps\mp\alien\_utility::any_player_nearby( self.origin, min_dist_walk_sqr ) )
|
|
{
|
|
next_movemode = "run";
|
|
}
|
|
else
|
|
{
|
|
weight_sum = 0;
|
|
rand_index = RandomIntRange( 0, total_weight );
|
|
next_movemode = undefined;
|
|
foreach ( movemode, weight in movemode_weight )
|
|
{
|
|
weight_sum += weight;
|
|
if ( rand_index <= weight_sum )
|
|
{
|
|
next_movemode = movemode;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
self set_alien_movemode( next_movemode );
|
|
wait ( randomFloatRange( min_change_time, max_change_time ) );
|
|
}
|
|
}
|
|
|
|
armorMitigation( vPoint, vDir, sHitLoc )
|
|
{
|
|
/*
|
|
switch ( sHitLoc )
|
|
{
|
|
case "head":
|
|
thread play_shield_impact_fx( vPoint, vDir );
|
|
return 0.5;
|
|
default:
|
|
return 1.0; // No mitigation
|
|
}
|
|
*/
|
|
}
|
|
|
|
play_shield_impact_fx( vPoint, vDir )
|
|
{
|
|
if ( !isDefined( vDir ) || !isDefined( vPoint ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
forward_vector = vDir * -1;
|
|
up_vector = anglesToUp ( vectorToAngles ( forward_vector ) );
|
|
PlayFX( level._effect[ "shield_impact" ], vPoint, forward_vector, up_vector );
|
|
self Playsound( "bullet_large_metal" );
|
|
}
|
|
|
|
has_shock_ammo ( sWeapon )
|
|
{
|
|
baseweapon = getRawBaseWeaponName( sWeapon );
|
|
if ( isDefined ( self.special_ammocount ) && isDefined ( self.special_ammocount[baseweapon] ) && self.special_ammocount[baseweapon] > 0 )
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
get_pet_follow_node( owner )
|
|
{
|
|
jump_nodes = GetNodesInRadius( owner.origin, 512, 128, 350, "node_jump" );
|
|
filters = [];
|
|
filters[ "direction" ] = "player_front";
|
|
filters[ "direction_weight" ] = 2.0;
|
|
filters[ "min_height" ] = -32.0;
|
|
filters[ "max_height" ] = 350.0;
|
|
filters[ "height_weight" ] = 2.0;
|
|
filters[ "enemy_los" ] = true;
|
|
filters[ "enemy_los_weight" ] = 2.0;
|
|
filters[ "min_dist_from_enemy" ] = 128.0;
|
|
filters[ "max_dist_from_enemy" ] = 512.0;
|
|
filters[ "desired_dist_from_enemy" ] = 350.0;
|
|
filters[ "dist_from_enemy_weight" ] = 3.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 200.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 0.0;
|
|
filters[ "not_recently_used_weight" ] = 4.0;
|
|
filters[ "random_weight" ] = 2.0;
|
|
next_node = get_retreat_node_rated( owner, filters, jump_nodes );
|
|
return next_node;
|
|
}
|
|
|
|
|
|
|
|
/#
|
|
debug_alien_ai_state( text )
|
|
{
|
|
if ( GetDvarInt( "alien_debug_ai_state" ) == 1 )
|
|
{
|
|
self thread debug_alien_ai_state_internal( text );
|
|
}
|
|
}
|
|
|
|
debug_alien_ai_state_internal( text )
|
|
{
|
|
self endon( "death" );
|
|
self notify( "debug_alien_ai_state" );
|
|
self endon( "debug_alien_ai_state" );
|
|
while ( 1 )
|
|
{
|
|
print3d( self.origin + ( 0, 0, 64 ), text, (.9, .3, .3), 1.0, 1.0 );
|
|
wait 0.05;
|
|
}
|
|
}
|
|
|
|
debug_alien_attacker_state( text )
|
|
{
|
|
if ( GetDvarInt( "alien_debug_ai_state" ) == 1 )
|
|
{
|
|
self thread debug_alien_attacker_state_internal( text );
|
|
}
|
|
}
|
|
|
|
debug_alien_attacker_state_internal( text )
|
|
{
|
|
self endon( "death" );
|
|
self endon ( "start retreat" );
|
|
self notify( "debug_alien_attacker_state" );
|
|
self endon( "debug_alien_attacker_state" );
|
|
while ( 1 )
|
|
{
|
|
print3d( self.origin + ( 0, 0, 84 ), text, (.3, .7, .7), 1.0, 1.0 );
|
|
wait 0.05;
|
|
}
|
|
}
|
|
|
|
alien_path_error( error_text )
|
|
{
|
|
if ( GetDvarInt( "alien_dev_path_error" ) == 1 )
|
|
{
|
|
error( error_text );
|
|
}
|
|
}
|
|
#/
|