842 lines
23 KiB
Plaintext
842 lines
23 KiB
Plaintext
#include common_scripts\utility;
|
|
#include maps\mp\alien\_utility;
|
|
|
|
ALIEN_SPIT_ATTACK_DISTANCE_MAX_SQ = 1440000; // 1200 * 1200
|
|
ALIEN_ESCAPE_SPIT_ATTACK_DISTANCE_MAX_SQ = 3240000; // 1800 * 1800
|
|
MIN_SPIT_TIMES = 3;
|
|
MAX_SPIT_TIMES = 6;
|
|
|
|
SPITTER_NODE_DURATION = 10; // Max length of time they stay at one spit node
|
|
SPITTER_FIRE_INTERVAL_MIN = 1.5; // Min amount of time in between a projectile fire
|
|
SPITTER_FIRE_INTERVAL_MAX = 3.0; // Max amount of time in between a projectile fire
|
|
SPITTER_PROJECTILE_BARRAGE_SIZE_MIN = 2; // Number of small projectiles to shoot at a time when not shooting a gas cloud
|
|
SPITTER_PROJECTILE_BARRAGE_SIZE_MAX = 3; // Number of small projectiles to shoot at a time when not shooting a gas cloud
|
|
|
|
SPITTER_GAS_CLOUD_FIRE_INTERVAL_MIN = 10.0; // Min amount of time in between a gas cloud projectile fire
|
|
SPITTER_GAS_CLOUD_FIRE_INTERVAL_MAX = 15.0; // Max amount of time in between a gas cloud projectile fire
|
|
SPITTER_GAS_CLOUD_MAX_COUNT = 3; // Max number of active gas clouds in a level
|
|
|
|
SPITTER_NODE_DAMAGE_DELAY = 0.1; // How long they wait to move from a spit node after getting damaged
|
|
SPITTER_MIN_PLAYER_DISTANCE_SQ = 90000.0; // If player gets within 300 units, they'll move to a new spit node
|
|
SPITTER_MOVE_MIN_PLAYER_DISTANCE_SQ = 40000.0; // If player gets within 200 units while alien is moving, they'll stop and spit a projectile at them
|
|
SPITTER_NODE_INITIAL_FIRE_DELAY_SCALE = 0.5; // Initial scale on delay before a spitter can spit after getting to a node
|
|
SPITTER_NO_TARGET_NODE_MOVE_TIME = 1.0; // If no targets at current node for this time, spitter will move
|
|
|
|
SPITTER_AOE_HEIGHT = 128; // Height of gas cloud
|
|
SPITTER_AOE_RADIUS = 150; // Radius of gas cloud
|
|
SPITTER_AOE_DURATION = 10.0; // How long the gas cloud lasts
|
|
SPITTER_AOE_DELAY = 2.0; // How long after projectile explodes before gas cloud damage is applied
|
|
SPITTER_AOE_DAMAGE_PER_SECOND = 12.0; // Damage per second at center of gas cloud
|
|
|
|
SPITTER_TIME_BETWEEN_SPITS = 3.33; // SPITTER_AOE_DURATION / SPITTER_GAS_CLOUD_MAX_COUNT
|
|
|
|
SPITTER_LOOK_AHEAD_PERCENTAGE = 0.5; // how accurately the spitters lead the players
|
|
SPITTER_ESCAPE_LOOK_AHEAD_PERCENTAGE = 1.0; // how accurately the spitters lead the players during escape sequence
|
|
|
|
load_spitter_fx()
|
|
{
|
|
level._effect[ "spit_AOE" ] = LoadFX( "vfx/gameplay/alien/vfx_alien_spitter_gas_cloud" );
|
|
level._effect[ "spit_AOE_small" ] = LoadFX( "vfx/gameplay/alien/vfx_alien_spitter_gas_cloud_64" );
|
|
}
|
|
|
|
spitter_init()
|
|
{
|
|
self.gas_cloud_available = true;
|
|
}
|
|
|
|
spitter_death()
|
|
{
|
|
release_spit_node();
|
|
}
|
|
|
|
is_escape_sequence_active()
|
|
{
|
|
return ( flag_exist( "hives_cleared" ) && flag( "hives_cleared" ) );
|
|
}
|
|
|
|
get_max_spit_distance_squared()
|
|
{
|
|
if ( is_escape_sequence_active() )
|
|
return ALIEN_ESCAPE_SPIT_ATTACK_DISTANCE_MAX_SQ;
|
|
|
|
return ALIEN_SPIT_ATTACK_DISTANCE_MAX_SQ;
|
|
}
|
|
|
|
get_lookahead_percentage()
|
|
{
|
|
if ( is_escape_sequence_active() )
|
|
return SPITTER_ESCAPE_LOOK_AHEAD_PERCENTAGE;
|
|
|
|
return SPITTER_LOOK_AHEAD_PERCENTAGE;
|
|
}
|
|
|
|
spit_projectile( enemy )
|
|
{
|
|
if ( self.spit_type == "gas_cloud" )
|
|
{
|
|
level.spitter_last_cloud_time = gettime();
|
|
}
|
|
|
|
self.melee_type = "spit";
|
|
|
|
self.spit_target = enemy;
|
|
maps\mp\agents\alien\_alien_think::alien_melee( enemy );
|
|
}
|
|
|
|
spit_attack( enemy )
|
|
{
|
|
/# maps\mp\agents\alien\_alien_think::debug_alien_ai_state( "spit_attack" ); #/
|
|
self endon( "melee_pain_interrupt" );
|
|
isEnemyChopper = isdefined( enemy ) && isdefined( enemy.code_classname ) && enemy.code_classname == "script_vehicle";
|
|
|
|
if ( isEnemyChopper )
|
|
targetedEnemy = enemy;
|
|
else
|
|
targetedEnemy = self.spit_target;
|
|
|
|
targetedEnemy endon( "death" );
|
|
self maps\mp\agents\alien\_alien_anim_utils::turnTowardsEntity( targetedEnemy );
|
|
|
|
if ( IsAlive( targetedEnemy ) )
|
|
{
|
|
self.spit_target = targetedEnemy;
|
|
|
|
if ( isEnemyChopper )
|
|
{
|
|
aim_ahead_factor = 5; // factor of speed MPH
|
|
aim_ahead_unit_vec = VectorNormalize( AnglesToForward( targetedEnemy.angles ) ); // direction
|
|
aim_ahead_speed_mag = Length( targetedEnemy Vehicle_GetVelocity() ) * aim_ahead_factor; // scaler
|
|
aim_ahead_vec = aim_ahead_unit_vec * aim_ahead_speed_mag; // aim ahead offset vector
|
|
|
|
self.spit_target_location = targetedEnemy.origin + aim_ahead_vec + ( 0, 0, 32 ); // offset by 32 down from origin as origin is at rotor
|
|
}
|
|
else
|
|
{
|
|
self.spit_target_location = targetedEnemy.origin;
|
|
}
|
|
|
|
self.looktarget = targetedEnemy;
|
|
|
|
self set_alien_emissive( 0.2, 1.0 );
|
|
|
|
if ( IsDefined ( self.current_spit_node ) && !maps\mp\alien\_utility::is_normal_upright( AnglesToUp( self.current_spit_node.angles ) ) )
|
|
{
|
|
up = AnglesToUp( self.current_spit_node.angles );
|
|
forward = AnglesToForward( self.angles );
|
|
left = VectorCross( up, forward );
|
|
forward = VectorCross( left, up );
|
|
right = (0,0,0) - left;
|
|
anglesToFace = AxisToAngles( forward, right, up );
|
|
self ScrAgentSetOrientMode( "face angle abs", anglesToFace );
|
|
}
|
|
else if ( IsDefined( self.enemy ) && targetedEnemy == self.enemy )
|
|
{
|
|
self ScrAgentSetOrientMode( "face enemy" );
|
|
}
|
|
else
|
|
{
|
|
forward = VectorNormalize( targetedEnemy.origin - self.origin );
|
|
if ( IsDefined( self.current_spit_node ) )
|
|
up = AnglesToUp( self.current_spit_node.angles );
|
|
else
|
|
up = AnglesToUp( self.angles );
|
|
left = VectorCross( up, forward );
|
|
forward = VectorCross( left, up );
|
|
right = (0,0,0) - left;
|
|
anglesToFace = AxisToAngles( forward, right, up );
|
|
self ScrAgentSetOrientMode( "face angle abs", anglesToFace );
|
|
}
|
|
|
|
if( self.oriented )
|
|
self ScrAgentSetAnimMode( "anim angle delta" );
|
|
else
|
|
self ScrAgentSetAnimMode( "anim deltas" );
|
|
play_spit_anim();
|
|
}
|
|
|
|
self set_alien_emissive_default( 0.2 );
|
|
self.looktarget = undefined;
|
|
self.spit_target = undefined;
|
|
self.spit_target_location = undefined;
|
|
self.spit_type = undefined;
|
|
}
|
|
|
|
play_spit_anim()
|
|
{
|
|
switch ( self.spit_type )
|
|
{
|
|
case "close_range":
|
|
self maps\mp\agents\_scriptedagents::PlayAnimUntilNotetrack( "close_spit_attack", "spit_attack", "end", ::handleAttackNotetracks );
|
|
break;
|
|
case "gas_cloud":
|
|
self maps\mp\agents\_scriptedagents::PlayAnimUntilNotetrack( "gas_spit_attack", "spit_attack", "end", ::handleAttackNotetracks );
|
|
break;
|
|
case "long_range":
|
|
barrage_count = RandomIntRange( SPITTER_PROJECTILE_BARRAGE_SIZE_MIN, SPITTER_PROJECTILE_BARRAGE_SIZE_MAX );
|
|
for ( spitIndex = 0; spitIndex < barrage_count; spitIndex++ )
|
|
self maps\mp\agents\_scriptedagents::PlayAnimUntilNotetrack( "long_range_spit_attack", "spit_attack", "end", ::handleAttackNotetracks );
|
|
break;
|
|
default:
|
|
AssertMsg( self.spit_type + " is an invalid spit type!" );
|
|
break;
|
|
}
|
|
}
|
|
|
|
get_best_spit_target( targeted_enemy )
|
|
{
|
|
if ( cointoss() && get_alien_type() != "seeder" )
|
|
{
|
|
griefTargets = get_grief_targets();
|
|
|
|
foreach ( griefTarget in griefTargets )
|
|
{
|
|
if ( is_valid_spit_target( griefTarget, false ) )
|
|
return griefTarget;
|
|
}
|
|
wait 0.05;
|
|
}
|
|
|
|
if ( IsDefined( targeted_enemy ) )
|
|
{
|
|
if ( IsAlive( targeted_enemy ) && is_valid_spit_target( targeted_enemy, false ) )
|
|
return targeted_enemy;
|
|
}
|
|
|
|
possibleTargets = self get_current_possible_targets();
|
|
MAX_TARGET_TESTS_PER_FRAME = 4;
|
|
currentTestsThisFrame = 0;
|
|
|
|
foreach ( possibleTarget in possibleTargets )
|
|
{
|
|
if ( !IsAlive( possibleTarget ) )
|
|
continue;
|
|
|
|
if ( IsDefined( targeted_enemy ) && possibleTarget == targeted_enemy )
|
|
continue;
|
|
|
|
if ( is_valid_spit_target( possibleTarget, true ) )
|
|
return possibleTarget;
|
|
|
|
currentTestsThisFrame++;
|
|
if ( currentTestsThisFrame >= MAX_TARGET_TESTS_PER_FRAME )
|
|
{
|
|
waitframe();
|
|
currentTestsThisFrame = 0;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
get_grief_targets()
|
|
{
|
|
griefTargets = [];
|
|
if ( !can_spit_gas_cloud( ) || is_pet())
|
|
return griefTargets;
|
|
|
|
foreach( player in level.players )
|
|
{
|
|
if ( !IsAlive( player ) )
|
|
continue;
|
|
|
|
if ( IsDefined( player.inLastStand ) && player.inLastStand )
|
|
griefTargets[griefTargets.size] = player;
|
|
}
|
|
|
|
if ( IsDefined( level.drill ) && IsDefined( level.drill.state ) && level.drill.state == "offline" )
|
|
griefTargets[griefTargets.size] = level.drill;
|
|
|
|
return array_randomize( griefTargets );
|
|
}
|
|
|
|
is_valid_spit_target( spit_target, check_attacker_values )
|
|
{
|
|
if ( !isAlive( spit_target ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( check_attacker_values && IsPlayer( spit_target ) && !has_attacker_space( spit_target ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
maxValidDistanceSq = get_max_spit_distance_squared();
|
|
|
|
flatDistanceToTargetSquared = Distance2DSquared( self.origin, spit_target.origin );
|
|
if ( flatDistanceToTargetSquared > maxValidDistanceSq )
|
|
return false;
|
|
|
|
self.looktarget = spit_target;
|
|
|
|
if ( !isAlive( spit_target ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (( isPlayer( spit_target ) || IsSentient( spit_target )) && !IsDefined( spit_target.usingRemote ) )
|
|
endPos = spit_target getEye();
|
|
else
|
|
endPos = spit_target.origin;
|
|
|
|
spitFirePos = self GetTagOrigin( "TAG_BREATH" );
|
|
return BulletTracePassed( spitFirePos, endPos, false, self );
|
|
}
|
|
|
|
get_spit_fire_pos( spit_target )
|
|
{
|
|
return self GetTagOrigin( "TAG_BREATH" );
|
|
}
|
|
|
|
has_attacker_space( player )
|
|
{
|
|
maxValidAttackerValue = level.maxAlienAttackerDifficultyValue - level.alien_types[ self.alien_type ].attributes[ "attacker_difficulty" ];
|
|
|
|
targetedAttackerScore = maps\mp\agents\alien\_alien_think::get_current_attacker_value( player );
|
|
|
|
return ( targetedAttackerScore <= maxValidAttackerValue );
|
|
}
|
|
|
|
handleAttackNotetracks( note, animState, animIndex, animTime )
|
|
{
|
|
if( isDefined( level.dlc_attacknotetrack_override_func ))
|
|
{
|
|
self [[level.dlc_attacknotetrack_override_func]]( note, animState, animIndex, animTime );
|
|
return;
|
|
}
|
|
|
|
if ( note == "spit" )
|
|
return self fire_spit_projectile();
|
|
}
|
|
|
|
fire_spit_projectile()
|
|
{
|
|
if ( !IsDefined( self.spit_target ) && !IsDefined( self.spit_target_location ) )
|
|
return;
|
|
|
|
hasValidTarget = IsAlive( self.spit_target );
|
|
isTargetChopper = isdefined( self.spit_target.code_classname ) && self.spit_target.code_classname == "script_vehicle";
|
|
if ( hasValidTarget && !isTargetChopper )
|
|
targetLocation = self.spit_target.origin;
|
|
else
|
|
targetLocation = self.spit_target_location;
|
|
|
|
if ( self.spit_type == "gas_cloud" )
|
|
{
|
|
spit_gas_cloud_projectile( targetLocation );
|
|
}
|
|
else if ( hasValidTarget )
|
|
{
|
|
PROJECTILE_SPEED = 1400;
|
|
targetLocation = get_lookahead_target_location( PROJECTILE_SPEED, self.spit_target, false );
|
|
if ( !BulletTracePassed( targetLocation, get_spit_fire_pos( targetLocation ), false, self ) )
|
|
targetLocation = get_lookahead_target_location( PROJECTILE_SPEED, self.spit_target, true );
|
|
|
|
spit_basic_projectile( targetLocation );
|
|
}
|
|
}
|
|
|
|
get_lookahead_target_location( projectile_speed, target, use_eye_location )
|
|
{
|
|
if ( !IsPlayer( target ) )
|
|
return target.origin;
|
|
|
|
lookAheadPercentage = get_lookahead_percentage();
|
|
|
|
if ( use_eye_location && !IsDefined( target.usingRemote ))
|
|
targetLocation = target GetEye();
|
|
else
|
|
targetLocation = target.origin;
|
|
|
|
distanceToTarget = Distance( self.origin, targetLocation);
|
|
timeToImpact = distanceToTarget / projectile_speed;
|
|
targetVelocity = target GetVelocity();
|
|
|
|
return targetLocation + targetVelocity * lookAheadPercentage * timeToImpact;
|
|
}
|
|
|
|
can_spit_gas_cloud()
|
|
{
|
|
if ( !self.gas_cloud_available )
|
|
return false;
|
|
|
|
if ( isdefined( self.enemy ) && isdefined( self.enemy.no_gas_cloud_attack ) && self.enemy.no_gas_cloud_attack )
|
|
return false;
|
|
|
|
time_since_last_spit = (gettime() - level.spitter_last_cloud_time) * 0.001;
|
|
|
|
return level.spitter_gas_cloud_count < SPITTER_GAS_CLOUD_MAX_COUNT && time_since_last_spit > SPITTER_TIME_BETWEEN_SPITS;
|
|
}
|
|
|
|
spit_basic_projectile( targetLocation )
|
|
{
|
|
spitFirePos = get_spit_fire_pos( targetLocation );
|
|
spitProjectile = MagicBullet( "alienspit_mp", spitFirePos, targetLocation, self );
|
|
spitProjectile.owner = self;
|
|
|
|
if ( IsDefined( spitProjectile ) )
|
|
spitProjectile thread spit_basic_projectile_impact_monitor( self );
|
|
}
|
|
|
|
spit_basic_projectile_impact_monitor( owner )
|
|
{
|
|
self waittill( "explode", explodeLocation );
|
|
|
|
if ( !IsDefined( explodeLocation ) )
|
|
return;
|
|
|
|
PlayFx( level._effect[ "spit_AOE_small" ], explodeLocation + (0,0,8), (0,0,1), (1,0,0) );
|
|
}
|
|
|
|
spit_gas_cloud_projectile( targetLocation )
|
|
{
|
|
spitFirePos = get_spit_fire_pos( targetLocation );
|
|
spitProjectile = MagicBullet( "alienspit_gas_mp", spitFirePos, targetLocation, self );
|
|
spitProjectile.owner = self;
|
|
|
|
if ( IsDefined( spitProjectile ) )
|
|
spitProjectile thread spit_gas_cloud_projectile_impact_monitor( self );
|
|
|
|
self thread gas_cloud_available_timer();
|
|
}
|
|
|
|
gas_cloud_available_timer()
|
|
{
|
|
self endon( "death" );
|
|
|
|
self.gas_cloud_available = false;
|
|
cloudInterval = RandomFloatRange( SPITTER_GAS_CLOUD_FIRE_INTERVAL_MIN, SPITTER_GAS_CLOUD_FIRE_INTERVAL_MAX );
|
|
wait cloudInterval;
|
|
self.gas_cloud_available = true;
|
|
}
|
|
|
|
spit_gas_cloud_projectile_impact_monitor( owner )
|
|
{
|
|
self waittill( "explode", explodeLocation );
|
|
|
|
if ( !IsDefined( explodeLocation ) )
|
|
return;
|
|
|
|
trigger = Spawn( "trigger_radius", explodeLocation, 0, SPITTER_AOE_RADIUS, SPITTER_AOE_HEIGHT );
|
|
// sanity check. Need to come up with more robust fallback
|
|
if ( !IsDefined( trigger ) )
|
|
return;
|
|
|
|
level.spitter_gas_cloud_count++;
|
|
trigger.onPlayer = true;
|
|
PlayFx( level._effect[ "spit_AOE" ], explodeLocation + (0,0,8),(0,0,1), (1,0,0) );
|
|
thread spit_aoe_cloud_damage( explodeLocation, trigger );
|
|
level notify( "spitter_spit",explodeLocation );
|
|
|
|
wait SPITTER_AOE_DURATION;
|
|
trigger Delete();
|
|
level.spitter_gas_cloud_count--;
|
|
}
|
|
|
|
spit_aoe_cloud_damage( impact_location, trigger )
|
|
{
|
|
trigger endon( "death" );
|
|
|
|
wait SPITTER_AOE_DELAY;
|
|
|
|
while ( true )
|
|
{
|
|
trigger waittill( "trigger", player );
|
|
|
|
if ( !IsPlayer( player ) )
|
|
continue;
|
|
|
|
if ( !IsAlive( player ) )
|
|
continue;
|
|
|
|
disorient_player( player );
|
|
damage_player( player, trigger );
|
|
}
|
|
}
|
|
|
|
damage_player( player, trigger )
|
|
{
|
|
DAMAGE_INTERVAL = 0.5;
|
|
|
|
currentTime = GetTime();
|
|
|
|
if ( !IsDefined( player.last_spitter_gas_damage_time ) )
|
|
{
|
|
elapsedTime = DAMAGE_INTERVAL;
|
|
}
|
|
else if (player.last_spitter_gas_damage_time + DAMAGE_INTERVAL * 1000.0 > currentTime )
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
elapsedTime = Min( DAMAGE_INTERVAL, (currentTime - player.last_spitter_gas_damage_time) * 0.001 );
|
|
}
|
|
|
|
gas_damage_scalar = player maps\mp\alien\_perk_utility::perk_GetGasDamageScalar();
|
|
damageAmount = int(SPITTER_AOE_DAMAGE_PER_SECOND * elapsedTime * gas_damage_scalar );
|
|
if ( damageAmount > 0 )
|
|
{
|
|
player thread [[ level.callbackPlayerDamage ]]( trigger, trigger, damageAmount, 0, "MOD_SUICIDE", "alienspit_gas_mp", trigger.origin, ( 0,0,0 ), "none", 0 );
|
|
}
|
|
player.last_spitter_gas_damage_time = currentTime;
|
|
}
|
|
|
|
disorient_player( player )
|
|
{
|
|
if ( is_chaos_mode() && player maps\mp\alien\_perk_utility::perk_GetGasDamageScalar() == 0 )
|
|
return;
|
|
else if( !player maps\mp\alien\_perk_utility::has_perk( "perk_medic", [ 1,2,3,4 ] ) )
|
|
{
|
|
if ( isDefined( level.shell_shock_override ))
|
|
player [[level.shell_shock_override]]( 0.5 );
|
|
else
|
|
player ShellShock( "alien_spitter_gas_cloud", 0.5 );
|
|
}
|
|
}
|
|
|
|
get_RL_toward( target )
|
|
{
|
|
//Return the right/left vector toward target
|
|
self_to_target_angles = VectorToAngles( target.origin - self.origin );
|
|
target_direction = anglesToRight( self_to_target_angles );
|
|
|
|
if ( common_scripts\utility::cointoss())
|
|
target_direction *= -1;
|
|
|
|
return target_direction;
|
|
}
|
|
|
|
spitter_combat( enemy )
|
|
{
|
|
self endon( "bad_path" );
|
|
self endon( "death" );
|
|
self endon ( "alien_main_loop_restart" );
|
|
|
|
while ( true )
|
|
{
|
|
attackNode = find_spitter_attack_node( self.enemy );
|
|
|
|
if ( IsDefined( attackNode ) )
|
|
{
|
|
move_to_spitter_attack_node( attackNode );
|
|
spitter_attack( self.enemy );
|
|
}
|
|
else
|
|
{
|
|
wait 0.05;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
release_spit_node()
|
|
{
|
|
if ( IsDefined( self.current_spit_node ) )
|
|
{
|
|
self ScrAgentRelinquishClaimedNode( self.current_spit_node );
|
|
self.current_spit_node.claimed = false;
|
|
self.current_spit_node = undefined;
|
|
}
|
|
}
|
|
|
|
claim_spit_node( spit_node )
|
|
{
|
|
self.current_spit_node = spit_node;
|
|
spit_node.claimed = true;
|
|
self ScrAgentClaimNode( spit_node );
|
|
}
|
|
|
|
move_to_spitter_attack_node( attack_node )
|
|
{
|
|
self endon( "player_proximity_during_move" );
|
|
|
|
release_spit_node();
|
|
claim_spit_node( attack_node );
|
|
|
|
self ScrAgentSetGoalNode( attack_node );
|
|
self ScrAgentSetGoalRadius( 64 );
|
|
self thread enemy_proximity_during_move_monitor();
|
|
self waittill( "goal_reached" );
|
|
}
|
|
|
|
enemy_proximity_during_move_monitor()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "goal_reached" );
|
|
self endon ( "alien_main_loop_restart" );
|
|
|
|
while ( true )
|
|
{
|
|
wait 0.05;
|
|
|
|
if ( !is_normal_upright( AnglesToUp( self.angles ) ) )
|
|
continue;
|
|
|
|
if ( !self maps\mp\agents\alien\_alien_think::melee_okay() )
|
|
continue;
|
|
|
|
if ( IsDefined( self.valid_moving_spit_attack_time ) && GetTime() < self.valid_moving_spit_attack_time )
|
|
continue;
|
|
|
|
closePlayer = find_player_within_distance( SPITTER_MOVE_MIN_PLAYER_DISTANCE_SQ );
|
|
if ( IsDefined( closePlayer ) )
|
|
break;
|
|
}
|
|
|
|
release_spit_node();
|
|
self notify( "player_proximity_during_move" );
|
|
self ScrAgentSetGoalEntity( closePlayer );
|
|
self ScrAgentSetGoalRadius( 2048.0 );
|
|
self waittill( "goal_reached" );
|
|
}
|
|
|
|
get_possible_spitter_attack_nodes( target_entity )
|
|
{
|
|
if( get_alien_type() == "seeder" )
|
|
attackNodes = GetNodesInRadius( target_entity.origin, 768, 128, 512, "jump attack" );
|
|
else
|
|
attackNodes = GetNodesInRadius( target_entity.origin, 1000, 300, 512, "jump attack" );
|
|
|
|
validNodes = [];
|
|
|
|
foreach( attackNode in attackNodes )
|
|
{
|
|
if ( IsDefined( attackNode.claimed) && attackNode.claimed )
|
|
continue;
|
|
|
|
validNodes[validNodes.size] = attackNode;
|
|
}
|
|
|
|
return validNodes;
|
|
|
|
}
|
|
|
|
is_pet()
|
|
{
|
|
return ( IsDefined( self.pet ) && self.pet );
|
|
}
|
|
|
|
get_current_possible_targets()
|
|
{
|
|
if ( is_pet() )
|
|
return level.agentArray;
|
|
else
|
|
return level.players;
|
|
}
|
|
|
|
find_spitter_attack_node( target_enemy )
|
|
{
|
|
nearbySpitNodes = [];
|
|
|
|
if ( is_escape_sequence_active() && IsDefined( level.escape_spitter_target_node ) )
|
|
{
|
|
nearbySpitNodes = get_possible_spitter_attack_nodes( level.escape_spitter_target_node );
|
|
if ( nearbySpitNodes.size > 0 )
|
|
target_enemy = level.escape_spitter_target_node;
|
|
}
|
|
|
|
if ( nearbySpitNodes.size == 0 && IsDefined( target_enemy ) )
|
|
nearbySpitNodes = get_possible_spitter_attack_nodes( target_enemy );
|
|
|
|
if ( nearbySpitNodes.size == 0 )
|
|
{
|
|
possibleTargets = self get_current_possible_targets();
|
|
foreach ( possibleTarget in possibleTargets )
|
|
{
|
|
wait 0.05;
|
|
|
|
if ( !IsAlive( possibleTarget ) )
|
|
continue;
|
|
|
|
if ( IsDefined( target_enemy ) && possibleTarget == target_enemy )
|
|
continue;
|
|
|
|
nearbySpitNodes = get_possible_spitter_attack_nodes( possibleTarget );
|
|
if ( nearbySpitNodes.size > 0 )
|
|
{
|
|
target_enemy = possibleTarget;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( nearbySpitNodes.size == 0 )
|
|
nearbySpitNodes = get_possible_spitter_attack_nodes( self );
|
|
|
|
if ( nearbySpitNodes.size == 0 )
|
|
return undefined;
|
|
|
|
filters = [];
|
|
|
|
if ( IsDefined( target_enemy ) )
|
|
{
|
|
filters[ "dist_from_enemy_weight" ] = 8.0;
|
|
filters[ "enemy_los_weight" ] = 6.0;
|
|
filters[ "height_weight" ] = 4.0;
|
|
target_direction = get_RL_toward( target_enemy );
|
|
target_enemy endon( "death" );
|
|
}
|
|
else
|
|
{
|
|
filters[ "dist_from_enemy_weight" ] = 0.0;
|
|
filters[ "enemy_los_weight" ] = 0.0;
|
|
filters[ "height_weight" ] = 0.0;
|
|
target_direction = get_central_enemies_direction();
|
|
}
|
|
|
|
filters[ "direction" ] = "override";
|
|
filters[ "direction_override" ] = target_direction;
|
|
filters[ "direction_weight" ] = 1.0;
|
|
filters[ "min_height" ] = 64.0;
|
|
filters[ "max_height" ] = 400.0;
|
|
filters[ "enemy_los" ] = true;
|
|
filters[ "min_dist_from_enemy" ] = 300.0;
|
|
filters[ "max_dist_from_enemy" ] = 800.0;
|
|
filters[ "desired_dist_from_enemy" ] = 600.0;
|
|
filters[ "min_dist_from_all_enemies" ] = 300.0;
|
|
filters[ "min_dist_from_all_enemies_weight" ] = 5.0;
|
|
filters[ "not_recently_used_weight" ] = 10.0;
|
|
filters[ "recently_used_time_limit" ] = 30.0;
|
|
filters[ "random_weight" ] = 1.0;
|
|
|
|
result = maps\mp\agents\alien\_alien_think::get_retreat_node_rated( target_enemy, filters, nearbySpitNodes );
|
|
|
|
return result;
|
|
}
|
|
|
|
get_central_enemies_direction()
|
|
{
|
|
possibleTargets = self get_current_possible_targets();
|
|
|
|
if ( possibleTargets.size == 0)
|
|
return self.origin + AnglesToForward( self.angles ) * 100;
|
|
|
|
centralLocation = ( 0, 0, 0 );
|
|
|
|
foreach ( possibleTarget in possibleTargets )
|
|
centralLocation += possibleTarget.origin;
|
|
|
|
centralLocation = centralLocation / possibleTargets.size;
|
|
|
|
return centralLocation - self.origin;
|
|
}
|
|
|
|
spitter_attack( enemy )
|
|
{
|
|
self endon( "spitter_node_move_requested" );
|
|
|
|
if ( !IsDefined( self.current_spit_node ) )
|
|
{
|
|
choose_spit_type( "close_range" );
|
|
spit_projectile( enemy );
|
|
self.valid_moving_spit_attack_time = GetTime() + RandomFloatRange( SPITTER_FIRE_INTERVAL_MIN, SPITTER_FIRE_INTERVAL_MAX ) * 1000.0;
|
|
return;
|
|
}
|
|
|
|
set_up_attack_node_watchers();
|
|
|
|
if ( !is_escape_sequence_active() )
|
|
wait RandomFloatRange( SPITTER_FIRE_INTERVAL_MIN, SPITTER_FIRE_INTERVAL_MAX ) * SPITTER_NODE_INITIAL_FIRE_DELAY_SCALE;
|
|
|
|
while ( true )
|
|
{
|
|
targetedEnemy = undefined;
|
|
no_target_time = 0.0;
|
|
while ( !IsDefined( targetedEnemy ) )
|
|
{
|
|
no_target_time += 0.2;
|
|
if ( no_target_time >= SPITTER_NO_TARGET_NODE_MOVE_TIME )
|
|
{
|
|
return;
|
|
}
|
|
wait 0.2;
|
|
|
|
if ( IsDefined( enemy ) && IsDefined( enemy.code_classname ) && enemy.code_classname == "script_vehicle" )
|
|
{
|
|
targetedEnemy = enemy;
|
|
}
|
|
else
|
|
{
|
|
targetedEnemy = get_best_spit_target( enemy );
|
|
}
|
|
}
|
|
|
|
choose_spit_type( "long_range" );
|
|
spit_projectile( targetedEnemy );
|
|
wait RandomFloatRange( SPITTER_FIRE_INTERVAL_MIN, SPITTER_FIRE_INTERVAL_MAX );
|
|
}
|
|
}
|
|
|
|
choose_spit_type( default_type )
|
|
{
|
|
if ( !is_pet() && can_spit_gas_cloud() )
|
|
self.spit_type = "gas_cloud";
|
|
else
|
|
self.spit_type = default_type;
|
|
}
|
|
|
|
set_up_attack_node_watchers()
|
|
{
|
|
self thread spitter_node_duration_monitor( SPITTER_NODE_DURATION );
|
|
self thread spitter_node_attacked_monitor( SPITTER_NODE_DAMAGE_DELAY );
|
|
|
|
if ( !is_pet() )
|
|
self thread spitter_node_player_proximity( SPITTER_MIN_PLAYER_DISTANCE_SQ );
|
|
}
|
|
|
|
spitter_node_duration_monitor( duration )
|
|
{
|
|
self endon( "spitter_node_move_requested" );
|
|
self endon( "death" );
|
|
self endon ( "alien_main_loop_restart" );
|
|
|
|
wait duration;
|
|
|
|
self notify( "spitter_node_move_requested" );
|
|
}
|
|
|
|
spitter_node_attacked_monitor( damage_delay )
|
|
{
|
|
self endon( "spitter_node_move_requested" );
|
|
self endon( "death" );
|
|
self endon ( "alien_main_loop_restart" );
|
|
|
|
self waittill( "damage" );
|
|
wait damage_delay;
|
|
|
|
self notify( "spitter_node_move_requested" );
|
|
}
|
|
|
|
spitter_node_player_proximity( min_player_distances_sq )
|
|
{
|
|
self endon( "spitter_node_move_requested" );
|
|
self endon( "death" );
|
|
self endon ( "alien_main_loop_restart" );
|
|
|
|
while ( true )
|
|
{
|
|
closePlayer = find_player_within_distance( min_player_distances_sq );
|
|
if ( IsDefined( closePlayer ) )
|
|
break;
|
|
|
|
wait 0.2;
|
|
}
|
|
|
|
self notify( "spitter_node_move_requested" );
|
|
}
|
|
|
|
find_player_within_distance( distance_sq )
|
|
{
|
|
foreach( player in level.players )
|
|
{
|
|
if ( !IsAlive( player ) )
|
|
continue;
|
|
|
|
flatDistanceToPlayerSq = Distance2DSquared( self.origin, player.origin );
|
|
if ( flatDistanceToPlayerSq < distance_sq )
|
|
return player;
|
|
}
|
|
|
|
return undefined;
|
|
} |