iw6-scripts-dev/maps/mp/alien/_spawnlogic.gsc
2024-12-11 11:28:08 +01:00

2270 lines
57 KiB
Plaintext

#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\agents\_agent_utility;
#include maps\mp\alien\_utility;
// ========================================================
// WAVE SPAWN
// ========================================================
/*
* - One spawner ent per AI type, spawner ent is moved to spawn locations to spawn AI
* - Spawn locations are script_structs; targetname = alien_spawn_struct; script_noteworthy = easy normal hard <- types
*/
CONST_DEFAULT_PREGAME_DELAY = 5; // time delay before first cycle starts after hitting wave trigger
CONST_DEFAULT_MAX_CYCLE_INTERMISSION = 150; // max time delay between cycles
CONST_DEFAULT_MIN_CYCLE_INTERMISSION = 90; // min time delay between cycles
CONST_DEFAULT_WAVE_INTERMISSION = 7; // time delay between waves
CONST_WAVE_END_WITH_ALIEN_NUM = 1; // number of aliens left in a wave before wave ends
CONST_MAX_LURKER_COUNT = 12; // when adding more lurkers, the max number of lurkers that will be in the level
CONST_MIN_LURKER_COUNT = 8; // when adding more lurkers, the min number of lurkers that will be in the level
/*
=============
///ScriptDocBegin
"Name: spawnAlien( origin, angles, alien_type )"
"Summary: Spawns an Alien"
"Module: Utility"
"MandatoryArg: <array>: Array of spawners"
"MandatoryArg: <func>: Function to run on the guy when he spawns"
"OptionalArg: <param1> : An optional parameter."
"OptionalArg: <param2> : An optional parameter."
"OptionalArg: <param3> : An optional parameter."
"OptionalArg: <param4> : An optional parameter."
"Example: spawnAlien( origin, target_angles, "drone" );"
"SPMP: multiplayer"
///ScriptDocEnd
=============
*/
spawnAlien( origin, angles, alien_type )
{
return maps\mp\gametypes\aliens::addAlienAgent( "axis", origin, angles, alien_type );
}
alien_health_per_player_init()
{
level.alien_health_per_player_scalar = [];
level.alien_health_per_player_scalar[ 1 ] = 0.9;
level.alien_health_per_player_scalar[ 2 ] = 1.0; // 20%
level.alien_health_per_player_scalar[ 3 ] = 1.3; // 20%
level.alien_health_per_player_scalar[ 4 ] = 1.8; // 20%
}
alien_wave_init()
{
// populates AI type attribute into level.alien_types[]
level.use_spawn_director = 1;
if ( use_spawn_director() )
{
maps\mp\alien\_spawn_director::init();
}
// init spawn locs
wave_spawners_init();
}
escape_choke_init()
{
level.choke_trigs = [];
level.choke_trigs[ 0 ] = GetEnt( "choke_trig_0", "targetname" );
level.choke_trigs[ 1 ] = GetEnt( "choke_trig_1", "targetname" );
level.choke_trigs[ 2 ] = GetEnt( "choke_trig_2", "targetname" );
level.choke_trigs[ 3 ] = GetEnt( "choke_trig_3", "targetname" );
if( !isdefined( level.choke_trigs ) || !level.choke_trigs.size )
return;
foreach ( trig in level.choke_trigs )
{
assert( isdefined( trig.target ) );
choke_point = getstruct( trig.target, "targetname" );
assert( isdefined( choke_point ) );
spit_at_struct = getstruct( choke_point.target, "targetname" );
assert( isdefined( spit_at_struct ) );
trig.choke_loc = choke_point.origin;
trig.spit_at_struct = spit_at_struct;
}
level thread monitor_trig_activation();
}
monitor_trig_activation()
{
level endon( "game_ended" );
if ( !flag_exist( "nuke_countdown" ) )
return;
level.latest_choke_trig_active = level.choke_trigs[ 0 ];
// assign initial spitter node
level.escape_spitter_target_node = level.choke_trigs[ 0 ].spit_at_struct;
if ( !IsDefined( level.escape_spitter_target_node.angles ) )
level.escape_spitter_target_node.angles = ( 0, 0, 0 );
if ( !flag( "nuke_countdown" ) )
flag_wait( "nuke_countdown" );
update_extraction_waypoint_location( level.latest_choke_trig_active get_choke_trig_id() );
// update level.latest_choke_trig_active
while ( 1 )
{
foreach ( player in level.players )
{
foreach ( trig in level.choke_trigs )
{
if ( isAlive( player ) && player IsTouching( trig ) && player.sessionstate == "playing" )
{
trig_id_num = trig get_choke_trig_id();
latest_trig_id_num = level.latest_choke_trig_active get_choke_trig_id();
if ( trig_id_num > latest_trig_id_num )
{
level.latest_choke_trig_active = trig;
update_extraction_waypoint_location( trig get_choke_trig_id() );
// assign next spitter node
level.escape_spitter_target_node = trig.spit_at_struct;
if ( !IsDefined( level.escape_spitter_target_node.angles ) )
level.escape_spitter_target_node.angles = ( 0, 0, 0 );
// next choke trig hit
thread escape_spawn_special_minion_wave( 6 );
}
}
}
}
wait 1;
}
}
update_extraction_waypoint_location( trig_idx )
{
assert( isdefined( trig_idx ) );
if ( !IsDefined( level.rescue_waypoint ) )
return;
level.rescue_waypoint.alpha = 1;
if ( !IsDefined( level.rescue_waypoint_locs ) || !IsDefined( level.rescue_waypoint_locs[ trig_idx ] ) )
return;
// start extract point location
level.rescue_waypoint.x = level.rescue_waypoint_locs[ trig_idx ][ 0 ];
level.rescue_waypoint.y = level.rescue_waypoint_locs[ trig_idx ][ 1 ];
level.rescue_waypoint.z = level.rescue_waypoint_locs[ trig_idx ][ 2 ];
}
escape_spawn_special_minion_wave( delay )
{
level endon( "game_ended" );
level endon( "nuke_went_off" );
level notify( "escape_spawn_special_minion_wave" );
level endon( "escape_spawn_special_minion_wave" );
wait delay;
notify_msg = "chaos_event_1"; // chaos_event_1 = minion wave
level notify( notify_msg );
//level.last_special_event_spawn_time = gettime();
maps\mp\alien\_spawn_director::activate_spawn_event( notify_msg );
/#
if ( GetDvarInt( "alien_debug_escape" ) > 0 )
IPrintLnBold( "^0[MINION METEOR][^7TRIGGERED^0]" );
#/
}
get_choke_trig_id()
{
// self is trig
return int( StrTok( self.targetname, "_" )[ 2 ] );
}
// ====================== scripted scenes =====================
alien_scene_init()
{
level.scene_trigs = getentarray( "scene_trig", "targetname" );
foreach ( trig in level.scene_trigs )
trig thread setup_scene();
}
setup_scene()
{
// self is trig
self.scene_spawners = getstructarray( self.target, "targetname" );
foreach ( spawner in self.scene_spawners )
{
// setup types
assert( isdefined( spawner.script_noteworthy ) && spawner.script_noteworthy !="" );
types = StrTok( spawner.script_noteworthy, " " );
assert( types.size );
spawner.types = types;
}
// run scene per trigger
self thread run_scene();
}
run_scene()
{
level endon( "game_ended" );
self endon( "death" ); // end on trigger removal
// self is trig
while ( 1 )
{
self waittill( "trigger", player );
if ( !isplayer( player ) )
{
wait 0.05;
continue;
}
spawners = array_randomize( self.scene_spawners );
for ( i = 0; i < spawners.size; i ++ )
{
if ( i > 0 )
{
// 50% chance of spawning
if ( randomint( 100 ) >= 50 )
continue;
}
spawner = spawners[ i ];
self thread spawn_scene_alien( spawner );
}
return;
}
}
spawn_scene_alien( spawner )
{
// self is trig
// spawn delay to offset
if ( isdefined( spawner.script_delay ) )
wait spawner.script_delay;
// random types
alien_type = "scene " + spawner.types[ randomintrange( 0, spawner.types.size ) ];
spawn_angles = spawner.angles;
if ( !isdefined( spawn_angles ) )
spawn_angles = ( 0, 270, 0 );
// spawn alien
alien = spawnAlien( spawner.origin, spawn_angles, alien_type );
alien.spawner = spawner;
}
alien_scene_behavior()
{
self endon( "death" );
self endon( "run_to_death" );
self maps\mp\agents\alien\_alien_think::set_alien_movemode( "run" );
old_rate = self.moveplaybackrate;
self.scene_moveplaybackrate = old_rate * 1.25;
self set_ignore_enemy();
/#
self thread alien_ai_debug_print( "acting" );
#/
self thread wakeup_to_player_damage();
self thread wakeup_to_player_distance( 256 );
self thread scene_loop();
self waittill( "woke" );
self.scene = undefined;
self.moveplaybackrate = old_rate;
self clear_ignore_enemy();
self ScrAgentSetScripted( false );
if ( self get_alien_type() == "elite" )
{
level thread maps\mp\alien\_music_and_dialog::handleFirstEliteArrival( self );
}
/#
self thread alien_ai_debug_print( "attacking" );
#/
wait 1; //wait for updates to agent combat variables
}
scene_loop()
{
self endon( "woke" );
self endon( "death" );
self endon( "run_to_death" );
wait 1;
self maps\mp\agents\alien\_alien_think::set_alien_movemode( "run" );
// start path, randomly picks path at forks
nodes = getstructarray( self.spawner.target, "targetname" );
assert( isdefined( nodes ) );
cur_node = nodes[ RandomIntRange( 0, nodes.size ) ];
while( isdefined( cur_node ) )
{
self ScrAgentSetGoalPos( cur_node.origin );
self ScrAgentSetGoalRadius( 32 );
self waittill( "goal_reached" );
if ( IsDefined( cur_node.script_noteworthy ) )
{
wait 0.5;
script_key = StrTok( cur_node.script_noteworthy, " " );
if ( script_key[ 0 ] == "delete" )
{
self suicide();
return;
}
// face players - pick a player
player = level.players[ RandomIntRange( 0, level.players.size ) ];
self maps\mp\agents\alien\_alien_anim_utils::turnTowardsEntity( player );
// times anims are played
anim_num = 1;
if ( script_key.size == 2 && int( script_key[ 1 ] ) > 1 )
anim_num = int( script_key[ 1 ] );
if ( script_key[ 0 ] == "posture" )
{
for( i = 0; i<anim_num; i++ )
{
self ScrAgentSetScripted( true );
self set_alien_emissive( 0.2, 1 );
self ScrAgentSetOrientMode( "face angle abs", self.angles );
self maps\mp\agents\_scriptedagents::PlayAnimUntilNotetrack( "posture", "posture", "end" );
self set_alien_emissive_default( 0.2 );
self ScrAgentSetScripted( false );
self ScrAgentSetGoalPos( self.origin );
wait RandomFloatRange( 0.25, 0.75 );
}
self.moveplaybackrate = self.scene_moveplaybackrate;
}
if ( script_key[ 0 ] == "ground_slam" )
{
for( i = 0; i<anim_num; i++ )
{
self ScrAgentSetScripted( true );
self set_alien_emissive( 0.2, 1 );
self ScrAgentSetOrientMode( "face angle abs", self.angles );
self maps\mp\agents\_scriptedagents::PlayAnimUntilNotetrack( "attack_melee_swipe", "attack_melee", "end" );
self set_alien_emissive_default( 0.2 );
self ScrAgentSetScripted( false );
self ScrAgentSetGoalPos( self.origin );
wait RandomFloatRange( 0.5, 1.0 );
}
self.moveplaybackrate = self.scene_moveplaybackrate;
}
}
// randomly picks path at fork
nodes = getstructarray( cur_node.target, "targetname" );
if ( !isdefined( nodes ) )
return;
cur_node = nodes[ RandomIntRange( 0, nodes.size ) ];
}
}
// ====================== scripted scenes [END] =====================
alien_lurker_init()
{
if ( !alien_mode_has( "lurker" ) )
return;
// mode defines when lurkers are suppose to be active
flag_init( "lurker_active" );
thread init_patrol_paths();
spawn_locs = getstructarray( "alien_spawn_struct", "targetname" );
// lurker spawners init
level.alien_lurkers = [];
level.max_lurker_population = 18;
// defined spawner types
foreach ( key_string, type in level.alien_types )
{
alien_lurker_struct = SpawnStruct();
alien_lurker_struct.spawn_locs = [];
level.alien_lurkers[ key_string ] = alien_lurker_struct;
}
foreach ( spawn_loc in spawn_locs )
{
msg_prefix = "Spawn location at: " + spawn_loc.origin + " ";
if ( isdefined( spawn_loc.script_noteworthy ) && spawn_loc.script_noteworthy != "" )
{
spawn_types = strtok( spawn_loc.script_noteworthy, " " );
// script_noteworthy exp: "lurker: brute cloaker spitter"
if( !IsSubStr( spawn_loc.script_noteworthy, "lurker" ) )
continue;
else
spawn_types = array_remove( spawn_types, spawn_types[ 0 ] ); // remove "lurker:"
// get spawn trigger
spawn_loc.spawn_trigger = getent( spawn_loc.target, "targetname" );
assert( IsDefined( spawn_loc.spawn_trigger ) );
// ASSERT as we are missing spawn types for this location!
assertex( spawn_types.size > 0, msg_prefix + "does not have enough spawn types" );
foreach ( type in spawn_types )
{
if ( isdefined( level.alien_lurkers[ type ] ) )
{
level.alien_lurkers[ type ].spawn_locs[ level.alien_lurkers[ type ].spawn_locs.size ] = spawn_loc;
}
else
{
assertex( true, msg_prefix + "has unknown spawn type: " + type );
}
}
}
else if ( !use_spawn_director() )
{
assertex( false, msg_prefix + "is missing spawner type (script_noteworthy = brute cloaker spitter)" );
}
}
level notify ( "alien_lurkers_spawn_initialized" );
/#
if ( !alien_mode_has( "nogame" ) )
thread lurker_loop();
#/
}
init_patrol_paths()
{
level.patrol_start_nodes = Getstructarray( "patrol_start_node", "targetname" );
}
// organize spawn locations into level.alien_wave[]
wave_spawners_init()
{
// spawn locations
level.alien_wave = [];
// defined spawner types
foreach ( key_string, type in level.alien_types )
{
alien_wave_struct = SpawnStruct();
alien_wave_struct.spawn_locs = [];
level.alien_wave[ key_string ] = alien_wave_struct;
}
alien_spawn_locs = getstructarray( "alien_spawn_struct", "targetname" );
assertex( isdefined( alien_spawn_locs ) && alien_spawn_locs.size > 0, "Not enough spawn locations (alien_spawn_struct)" );
foreach ( spawn_loc in alien_spawn_locs )
{
msg_prefix = "Spawn location at: " + spawn_loc.origin + " ";
if ( isdefined( spawn_loc.script_noteworthy ) && spawn_loc.script_noteworthy != "" )
{
spawn_types = strtok( spawn_loc.script_noteworthy, " " );
// ignore lurker spawn locs
if( IsSubStr( spawn_loc.script_noteworthy, "lurker" ) )
continue;
// ASSERT as we are missing spawn types for this location!
assertex( spawn_types.size > 0, msg_prefix + "does not have enough spawn types" );
foreach ( type in spawn_types )
{
if ( isdefined( level.alien_wave[ type ] ) )
level.alien_wave[ type ].spawn_locs[ level.alien_wave[ type ].spawn_locs.size ] = spawn_loc;
else
assertex( true, msg_prefix + "has unknown spawn type: " + type );
}
}
else if ( !use_spawn_director() )
{
assertex( false, msg_prefix + "is missing spawner type (script_noteworthy = brute cloaker spitter)" );
}
}
level notify ( "alien_wave_spawn_initialized" );
}
assign_alien_attributes( alien_type )
{
// self is spawned AI
self endon( "death" );
spawn_type = "none";
// if spawn type is passed in, it is the first token delimited by space
spawn_type_config = strtok( alien_type, " " );
if ( isdefined( spawn_type_config ) && spawn_type_config.size == 2 )
{
if ( spawn_type_config[ 0 ] == "lurker" )
spawn_type = "lurker";
if ( spawn_type_config[ 0 ] == "wave" )
spawn_type = "wave";
if ( spawn_type_config[ 0 ] == "scene" )
spawn_type = "scene";
alien_type = spawn_type_config[ 1 ];
}
assertex( spawn_type != "none", "spawn type defined does not exist - none" );
assertex( isdefined( level.alien_types ), "Must run _alien::main() before _spawner::wave_spawners_init()" );
// set health
if ( isDefined( level.players ) && level.players.size > 0 )
{
health = level.alien_types[ alien_type ].attributes[ "health" ] * level.alien_health_per_player_scalar[ level.players.size ];
}
else
{
health = level.alien_types[ alien_type ].attributes[ "health" ];
}
health = int( level.cycle_health_scalar * health );
self maps\mp\agents\_agent_common::set_agent_health( health );
self.max_health = health;
self.alien_type = level.alien_types[ alien_type ].attributes[ "ref" ];
self.moveplaybackrate = level.alien_types[ alien_type ].attributes[ "speed" ] * RandomFloatRange( 0.95, 1.05 );
// escape mode = faster aliens!
if ( flag_exist( "hives_cleared" ) && flag( "hives_cleared" ) )
self.moveplaybackrate *= 1.25;
self.defaultmoveplaybackrate = self.moveplaybackrate;
self.animplaybackrate = self.moveplaybackrate;
self.xyanimscale = level.alien_types[ alien_type ].attributes[ "scale" ];
self.hindlegstraceoffset = -37.0 * self.xyanimscale; // Used to pitch alien so hind legs hit the ground
self.defaultEmissive = level.alien_types[ alien_type ].attributes[ "emissive_default" ];
self.maxEmissive = level.alien_types[ alien_type ].attributes[ "emissive_max" ];
self thread set_initial_emissive();
self ScrAgentSetViewHeight( level.alien_types[ alien_type ].attributes[ "view_height" ] );
if ( spawn_type == "lurker" )
{
self thread alien_lurker_behavior();
self.lurker = true;
}
if ( spawn_type == "scene" )
{
self thread alien_scene_behavior();
self.scene = true;
}
if ( spawn_type == "wave" )
{
// TODO: alien behavior in MP needs to be worked on
self thread alien_wave_behavior();
self.wave_spawned = true;
}
// set the aliens on fire
// self delayThread(1.0, ::add_fire_fx);
// TEMP: disable cloaking
// TODO: reenable cloaking after fx is pretty
if ( level.alien_types[ alien_type ].attributes[ "behavior_cloak" ] == 1 )
{
self thread maps\mp\alien\_director::alien_cloak();
}
}
set_initial_emissive()
{
self endon( "death" );
wait 1;
set_alien_emissive_default( 0.2 );
}
// set an alien on fire call on the alien itself
add_fire_fx()
{
self maps\mp\alien\_alien_fx::alien_fire_on();
}
// stop alien fire
stop_fire_fx()
{
self maps\mp\alien\_alien_fx::alien_fire_off();
}
// manage remaining aliens after a cycle is over
remaining_alien_management()
{
aliens = GetActiveAgentsOfType( "alien" );
foreach ( alien in aliens )
{
if ( !isalive( alien ) )
continue;
if ( isDefined( alien.from_previous_hive ) )
{
alien suicide();
}
else
{
alien.from_previous_hive = true;
}
if ( !isdefined( alien.alien_type ) || alien.alien_type == "elite" )
continue;
if ( alien.health < 80 )
continue;
// do damage on these alive agents
hp_remove_factor = 0.5;
hp_remove = int( hp_remove_factor * alien.health );
alien.health -= hp_remove;
wait 0.05;
}
}
// [START] ====================== BLOCKER HIVE LOGIC ======================
get_blocker_hive( hive_id )
{
foreach ( hive_loc in level.stronghold_hive_locs )
{
if ( isdefined( hive_loc.target ) && hive_loc.target == hive_id )
return hive_loc;
}
return undefined;
}
make_spitter_attack_chopper( attack )
{
// self is spitter
if ( attack )
group = "spitters";
else
group = "other_aliens";
if ( ThreatBiasGroupExists( group ) && self GetThreatBiasGroup() != group )
self SetThreatBiasGroup( group );
if ( isdefined( level.hive_heli ) && attack )
{
if ( !isdefined( self.favoriteenemy ) || self.favoriteenemy != level.hive_heli )
self.favoriteenemy = level.hive_heli;
}
}
// [END] ====================== BLOCKER HIVE LOGIC ======================
// infinite cycle!
escape_spawning( cycle_count )
{
// crank the threat
foreach ( player in level.players )
player.threatbias = 100000;
maps\mp\alien\_spawn_director::start_cycle( cycle_count );
level thread clean_up_lagged_aliens();
level waittill_any( "game_ended", "nuke_went_off" );
maps\mp\alien\_spawn_director::end_cycle();
// delay
if ( flag_exist( "nuke_went_off" ) && flag( "nuke_went_off" ) )
level waittill_any_timeout( 10, "game_ended" );
else
wait 7; // game ended, meaning players failed by dying before nuke went off
// kill all aliens
foreach ( agent in level.agentArray )
{
if ( isalive( agent ) && isDefined( agent.isActive ) && agent.isActive )
{
agent Suicide();
wait 0.1;
}
}
}
// clean up lagged aliens so players are getting constant resistance
CONST_TIME_UNSEEN_RESET = 2;
CONST_TIME_ALIVE_RESET = 10;
CONST_LAGGED_RADIUS = 1000;
clean_up_lagged_aliens()
{
level endon( "nuke_went_off" );
level endon( "game_ended" );
level notify( "clean_up_lagged_aliens" );
level endon( "clean_up_lagged_aliens" );
interval = 1;
while ( 1 )
{
active_aliens = [];
foreach ( agent in level.agentArray )
{
// only active
if ( !IsDefined( agent.isActive ) || !agent.isActive )
continue;
active_aliens[ active_aliens.size ] = agent;
}
buffer = 4;
if ( active_aliens.size <= ( maps\mp\alien\_spawn_director::get_max_alien_count() - buffer ) )
{
wait 1;
continue;
}
foreach ( alien in active_aliens )
{
if ( !isdefined( alien.last_looked_at ) )
{
alien.last_looked_at = gettime(); // can't be sure if recently seen, so seen as initial state to be safe
alien thread monitor_looked_at();
continue;
}
// (1 only enemy =================================
if ( !isdefined( alien.team ) || alien.team != "axis" )
continue; // 1)
// (2 only alive long enough =====================
alive_time = gettime() - alien.birthtime;
if ( alive_time / 1000 < CONST_TIME_ALIVE_RESET )
continue; // 2)
// (3 only if far from players ===================
result = false;
foreach ( player in level.players )
{
if ( isalive( player ) && distance( player.origin, alien.origin ) < CONST_LAGGED_RADIUS )
result = true;
}
if ( result )
continue; // 3)
// (4 only if unseen for a period of time ========
time_unseen = gettime() - alien.last_looked_at;
if ( time_unseen / 1000 > CONST_TIME_UNSEEN_RESET )
{
/#
if ( GetDvarInt( "alien_debug_escape" ) > 0 )
IPrintLnBold( "^1[ALIEN CLEANED AT: " + alien.origin + "]" );
#/
alien Suicide();
//kill alien, break loop, kill one at a time, so we can check against buffer again
break; // 4)
}
}
wait interval;
}
}
monitor_looked_at()
{
self endon( "death" );
self notify( "monitoring_looked_at" );
self endon( "monitoring_looked_at" );
while ( 1 )
{
foreach ( player in level.players )
{
if ( !isalive( player ) )
continue;
angles = player gettagangles( "tag_eye" );
origin = player getEye();
sight = anglestoforward( angles );
vec = vectornormalize( self.origin - origin );
cone = 0.55;
if ( VectorDot( sight, vec ) > cone )
self.last_looked_at = gettime();
}
wait 0.25;
}
}
// radius to teleport alien in front of players
CONST_TELEPORT_TO_PLAYER_RADIUS = 350; // initial radius to look for locs to spawn
CONST_TELEPORT_TO_PLAYER_RADIUS_INCREMENT = 250; // radius increment when no spawn locs found
CONST_TELEPORT_TO_PLAYER_RADIUS_MAX = 2000; // max radius to sample locs for spawn
CONST_PORT_ALINE_TO_LEADER_CHANCE = 66; // % to distribute alien to the farthest ahead player
// get position to teleport alien to player, biased to player closest to escape position
port_to_player_loc( alien_type, radius )
{
assertex( isdefined( level.choke_trigs ), "There are no choke triggers found in map..." );
if ( !isdefined( radius ) )
radius = CONST_TELEPORT_TO_PLAYER_RADIUS;
// spawn minions at modified distance
if ( isDefined( alien_type ) && alien_type == "minion" )
radius *= 1.2;
alive_players = [];
foreach ( player in level.players )
{
if ( isalive( player ) )
alive_players[ alive_players.size ] = player;
}
/#
//TEMP DEBUG
if ( GetDvarInt( "alien_debug_escape" ) > 0 && alive_players.size == 0 )
{
IPrintLnBold( "DEBUG: No player(s) found alive to teleport alien to" );
return undefined;
}
#/
// ------------------------- SORT PLAYERS BY DIST TO ESCAPE POINT ------------------------
sorted_players = [];
// count backwards because it is the order closest to the escape point
for( i = level.choke_trigs.size - 1; i >= 0; i-- )
{
trig = level.choke_trigs[ i ];
occupants = [];
foreach ( player in alive_players )
{
if ( player istouching( trig ) )
occupants[ occupants.size ] = player;
}
// sort closest to choke among occupants
if ( occupants.size )
{
occupants = SortByDistance( occupants, trig.choke_loc );
for( j = 0; j < occupants.size; j++ )
sorted_players[ sorted_players.size ] = occupants[ j ];
}
}
/#
//TEMP DEBUG
if ( GetDvarInt( "alien_debug_escape" ) > 0 && alive_players.size == 0 )
{
IPrintLnBold( "DEBUG: No player(s) found in choke triggers" );
return undefined;
}
#/
// 66% chance of it being the closest player
selected_player = sorted_players[ 0 ];
if ( sorted_players.size > 1 && RandomIntRange( 0, 100 ) <= CONST_PORT_ALINE_TO_LEADER_CHANCE )
selected_player = sorted_players[ RandomIntRange( 1, sorted_players.size ) ];
// ---------------------------------------------------------------------------------------
choke_loc = undefined;
foreach ( trig in level.choke_trigs )
{
if ( selected_player istouching( trig ) )
{
choke_loc = trig.choke_loc;
break;
}
}
if ( !isdefined( choke_loc ) )
{
/#
if ( GetDvarInt( "alien_debug_escape" ) > 0 )
IPrintLnBold( "Player at: " + selected_player.origin + " is not inside any choke trigger" );
#/
return undefined;
}
// get spawn locs
centering_factor = 3; // this factor moves the sample circle this much closer to player (%, 0=player is at center, 1=player is at edge)
if ( alien_type == "spitter" || alien_type == "seeder" )
centering_factor = 4;
origin_infront_of_player = selected_player.origin + VectorNormalize( choke_loc - selected_player.origin ) * radius * centering_factor;
nodes = GetNodesInRadius( origin_infront_of_player, radius, 0, 1024, "Path" );
/#
if ( GetDvarInt( "alien_debug_escape" ) > 0 )
thread debug_circle( origin_infront_of_player, radius, (0,1,1), true, 16, 1 );
#/
if ( nodes.size == 0 )
{
if ( radius >= CONST_TELEPORT_TO_PLAYER_RADIUS_MAX )
{
/#
if ( GetDvarInt( "alien_debug_escape" ) > 0 )
IPrintLnBold( "DEBUG: No spawn node found near player at: " + selected_player.origin + " within 3000 radius" );
#/
return undefined;
}
new_radius = radius + CONST_TELEPORT_TO_PLAYER_RADIUS_INCREMENT;
/#
if ( GetDvarInt( "alien_debug_escape" ) > 0 )
IPrintLnBold( "DEBUG: No spawn node found near player at: " + selected_player.origin + ", new radius=" + new_radius );
#/
return port_to_player_loc( alien_type, new_radius );
}
// node to teleport to
selected_node = get_selected_node_for_teleport( nodes, choke_loc, selected_player );
/#
if ( GetDvarInt( "alien_debug_escape" ) > 1 )
{
position = "#";
foreach ( index, player in sorted_players )
{
if ( player == selected_player )
position = int( index + 1 );
}
population = 1; // counts self who is not yet active
foreach ( agent in level.agentArray )
{
if ( !IsDefined( agent.isActive ) || !agent.isActive || !isalive( agent ) )
continue;
population++;
}
if ( isdefined( selected_player.name ) && isdefined( selected_node ) && isdefined( selected_node.origin ) )
iprintln( "[^3"+alien_type+"^7 ported to ^3"+selected_player.name+"^7 in position ^3"+position+"^7][dist=^3"+int(distance(selected_node.origin,selected_player.origin))+"^7][alien#=^3"+population+"^7]" );
}
#/
angles_facing_player = VectorToAngles( selected_player.origin - selected_node.origin );
return [ selected_node.origin, angles_facing_player ];
}
port_to_escape_spitter_location()
{
SPITTER_NODE_RADIUS = 512;
spitterLocation = level.escape_spitter_target_node.origin;
nodes = GetNodesInRadius( spitterLocation, SPITTER_NODE_RADIUS, 0, 1024, "Path" );
selectedNode = get_selected_node_for_teleport( nodes, spitterLocation, level.escape_spitter_target_node );
return selectedNode.origin;
}
get_selected_node_for_teleport( nodes, location, selected_target )
{
// avaliable nodes to teleport to
nodes = SortByDistance( nodes, location );
selected_node = undefined;
for ( i = 0; i < nodes.size; i ++ )
{
if ( !isdefined( nodes[ i ].teleport_inuse ) || !nodes[ i ].teleport_inuse )
{
selected_node = nodes[ i ];
selected_node.teleport_inuse = true;
thread reset_teleport_inuse( selected_node );
break;
}
}
if ( !isdefined( selected_node ) )
{
/#
if ( GetDvarInt( "alien_debug_escape" ) > 0 )
IPrintLnBold( "All path nodes near player at: " + selected_target.origin + " are in cooldown for teleport" );
#/
selected_node = nodes[ randomint( nodes.size ) ]; // random
}
return selected_node;
}
debug_circle( center, radius, color, depthTest, segments, time )
{
/#
level endon( "game_ended" );
if ( !isDefined( time ) )
time = 0.5;
if ( !isDefined( segments ) )
segments = 16;
angleFrac = 360/segments;
circlepoints = [];
for( i = 0; i < segments; i++ )
{
angle = (angleFrac * i);
xAdd = cos(angle) * radius;
yAdd = sin(angle) * radius;
x = center[0] + xAdd;
y = center[1] + yAdd;
z = center[2];
circlepoints[circlepoints.size] = ( x, y, z );
}
while ( time > 0 )
{
for( i = 0; i < circlepoints.size; i++ )
{
start = circlepoints[i];
if (i + 1 >= circlepoints.size)
end = circlepoints[0];
else
end = circlepoints[i + 1];
line( start, end, color, 1.0, depthTest );
}
time -= 0.05;
wait 0.05;
}
#/
}
reset_teleport_inuse( node )
{
wait 5;
node.teleport_inuse = undefined;
}
// infinite cycle!
infinite_cycle( cycle_count )
{
maps\mp\alien\_spawn_director::start_cycle( cycle_count );
level waittill( "game_ended" );
maps\mp\alien\_spawn_director::end_cycle();
}
/**************************/
/* Main lurker spawn loop */
/**************************/
lurker_loop()
{
level endon( "game_ended" );
//lurker_spawn_delay = 0; // seconds delay before lurker spawn triggers are active
//delayThread( lurker_spawn_delay, ::flag_set, "lurker_active" );
// assuming mode doesn't start with mist on
flag_set( "lurker_active" );
while ( true )
{
// lurker settings
ai_event_settings();
// TODO: define logic for which types of lurker to spawn
type = "goon";
foreach ( lurker_spawn_struct in level.alien_lurkers[ type ].spawn_locs )
{
lurker_spawn_struct notify( "new_listener" );
lurker_spawn_struct thread lurker_listen_trigger( type );
}
// ^
// during this time, the lurkers lurk!
// v
level waittill( "alien_cycle_prespawning", countdown );
ahead_of_time = 10; // seconds ahead before wave starts
wait ( max( 0, countdown - ahead_of_time ) );
flag_clear( "lurker_active" );
level notify( "removing_lurkers" );
// start to get rid of lurkers =========================
ai_event_settings_reset(); // restore event settings
alive_lurkers = get_alive_lurkers();
foreach ( lurker in alive_lurkers )
lurker thread send_away_and_die();
wait 6; // usually takes less thand 6 seconds to complete send_away_and_die();
// fail-safe: we have to kill all lurkers to make room for wave!
if ( get_alive_lurkers().size > 0 )
{
foreach( lurker in get_alive_lurkers() )
lurker Suicide();
}
// lurkers all dead, prep for wave spawn ===============
// wait till waves are done, restore lurkers
level waittill( "alien_cycle_ended" );
flag_set( "lurker_active" );
}
}
// distance where lurker becomes alerted
CONST_LURKER_ALERT_DIST = 512;
CONST_LURKER_GUNSHOT_ALERT_DIST = 512;
ai_event_settings()
{
level.old_ai_eventdistgunshot = GetDvarInt( "ai_eventdistgunshot" );
level.old_ai_eventdistgunshotteam = GetDvarInt( "ai_eventdistgunshotteam" );
level.old_ai_eventdistnewenemy = GetDvarInt( "ai_eventdistnewenemy" );
level.old_ai_eventdistdeath = GetDvarInt( "ai_eventdistdeath" );
SetDvar( "ai_eventdistgunshot", CONST_LURKER_GUNSHOT_ALERT_DIST );
SetDvar( "ai_eventdistgunshotteam", CONST_LURKER_GUNSHOT_ALERT_DIST );
SetDvar( "ai_eventdistnewenemy", CONST_LURKER_ALERT_DIST );
SetDvar( "ai_eventdistdeath", CONST_LURKER_ALERT_DIST );
}
ai_event_settings_reset()
{
SetDvar( "ai_eventdistgunshot", level.old_ai_eventdistgunshot );
SetDvar( "ai_eventdistgunshotteam", level.old_ai_eventdistgunshotteam );
SetDvar( "ai_eventdistnewenemy", level.old_ai_eventdistnewenemy );
SetDvar( "ai_eventdistdeath", level.old_ai_eventdistdeath );
}
// removes lurkers farthest from players - instantly
remove_farthest_lurker( avoid_ents )
{
lurkers = get_alive_lurkers();
while( lurkers.size > 1 )
{
foreach ( avoid_ent in avoid_ents )
{
far_agent = getclosest( avoid_ent.origin, lurkers );
lurkers = array_remove( lurkers, far_agent );
if ( lurkers.size == 1 )
break;
}
}
if ( !lurkers.size )
return false;
lurkers[ 0 ] Suicide();
}
get_alive_agents()
{
alive_agents = [];
foreach ( agent in level.agentArray )
{
if ( isalive( agent ) )
alive_agents[ alive_agents.size ] = agent;
}
return alive_agents;
}
// Returns an array of alive agent AND non-agent enemies.
get_alive_enemies()
{
alive_enemies = [];
alive_non_agents = [];
alive_agents = get_alive_agents();
// Check for level specific func to get non agent enemies
if( isDefined( level.dlc_get_non_agent_enemies ))
alive_non_agents = [[level.dlc_get_non_agent_enemies]]();
alive_enemies = array_combine( alive_agents, alive_non_agents );
return alive_enemies;
}
get_alive_lurkers()
{
lurkers = [];
foreach ( agent in level.agentArray )
{
if ( isalive( agent ) && isdefined( agent.lurker ) )
lurkers[ lurkers.size ] = agent;
}
return lurkers;
}
CONST_LURKER_RESPAWN_TIME = 10;
// lurker's spawn trigger loop
lurker_listen_trigger( type )
{
level endon( "game_ended" );
level endon( "removing_lurkers" );
self endon( "new_listener" );
// spawn cool down after spawning
self.cooldown = 0;
self.spawned_lurker = undefined;
self.spawn_trigger.reset = false; // false, dont spawn lurkers near player spawn, spawn when player return
// self is spawn_loc
while ( true )
{
// only spawn lurkers when allowed by mode
if ( !flag( "lurker_active" ) )
{
flag_wait( "lurker_active" );
self.cooldown = 0; // lurkers turned on, all spawn on trigger, previous cool down reset
}
wait self.cooldown;
self.cooldown = 0;
// if lurker already spawned here and is still alive
if ( isdefined( self.spawned_lurker ) && isalive( self.spawned_lurker ) )
{
wait 0.05;
continue;
}
if ( !self.spawn_trigger.reset )
self wait_for_reset();
self.spawn_trigger waittill( "trigger", owner );
self.spawn_trigger.reset = false;
// if we are over budget with agents, remove lurkers
if ( get_alive_agents().size >= get_alive_lurkers().size )
{
while( get_alive_agents().size >= level.max_lurker_population )
{
remove_farthest_lurker( get_players() );
wait 0.1;
}
}
else
{
while( get_alive_lurkers().size >= level.max_lurker_population )
{
remove_farthest_lurker( get_players() );
wait 0.1;
}
}
self.cooldown = CONST_LURKER_RESPAWN_TIME; // cool down for next lurker to spawn after previous one is killed
// spawn the lurker!
self.spawned_lurker = self spawn_lurker( type );
// pause loop till death
self.spawned_lurker waittill( "death" );
}
}
// wait for everyone out of the trigger!
wait_for_reset()
{
level endon( "game_ended" );
level endon( "removing_lurkers" );
self endon( "new_listener" );
is_touching = true;
while ( is_touching )
{
is_touching = false;
foreach ( player in get_players() )
{
if ( player IsTouching( self.spawn_trigger ) )
is_touching = true;
}
if ( is_touching )
wait 0.05;
}
self.spawn_trigger.reset = true;
}
// sends lurker away from players to get deleted
// this is to make sure they aren't left to attack during mist hit
send_away_and_die()
{
self endon( "death" );
self notify( "run_to_death" );
/#
if ( isdefined( self.lurker ) )
self thread alien_ai_debug_print( "Lurker x_X" );
else
self thread alien_ai_debug_print( "x_X" );
#/
// send away
//self run_cycle(); // breaks slow near player functions
self maps\mp\agents\alien\_alien_think::set_alien_movemode( "run" );
self set_ignore_enemy();
self.moveplaybackrate *= 1.25;
far_nodes = GetNodesInRadiusSorted( self.origin, 1000, 10 );
// go somewhere
self ScrAgentSetGoalNode( far_nodes[ far_nodes.size - 1 ] );
self ScrAgentSetGoalRadius( 64 );
self waittill_any_timeout( 5, "goal_reached" );
self clear_ignore_enemy();
// cloak and delete
smoke_puff();
wait 0.20;
//TODO: move them outside the map and remove
// kill the agent
self Suicide();
return true;
}
set_ignore_enemy()
{
self enable_alien_scripted();
if ( isdefined( self.enemy ) )
self.enemy.current_attackers = [];
}
clear_ignore_enemy()
{
self disable_alien_scripted();
foreach ( player in get_players() )
self GetEnemyInfo( player );
}
smoke_puff()
{
PlayFXOnTag( level._effect[ "alien_teleport" ], self, "j_jaw" );
PlayFXOnTag( level._effect[ "alien_teleport" ], self, "j_spineupper" );
PlayFXOnTag( level._effect[ "alien_teleport" ], self, "j_mainroot" );
PlayFXOnTag( level._effect[ "alien_teleport" ], self, "j_tail_3" );
PlayFXOnTag( level._effect[ "alien_teleport_dist" ], self, "j_mainroot" );
}
spawn_lurker( type )
{
// self is spawn_loc
assert( isdefined( self ) );
// MP spawning ===========================
spawner_origin = self.origin;
spawner_angles = ( 0, 0, 0 );
if ( isdefined( self.angles ) )
spawner_angles = self.angles;
spawn_type = "lurker";
spawn_type_config = spawn_type + " " + type;
agent = spawnAlien( spawner_origin, spawner_angles, spawn_type_config );
return agent;
}
// WIP seek behavior
alien_wave_behavior()
{
self endon( "death" );
// Get perfect knowledge of enemies for now
foreach ( player in get_players() )
{
self GetEnemyInfo( player );
}
// Attack planted bomb if it exists
if ( isdefined( level.bomb) && IsSentient( level.bomb ) )
self GetEnemyInfo( level.bomb );
self.goalradius = 64;
self.inStandingMelee = false;
/#
self thread alien_ai_debug_print( self.alien_type );
#/
//self thread watch_alien_death();
//self thread alien_aggress_sound();
//self thread watch_alien_damage();
//self.allowpain = false;
}
alien_lurker_behavior()
{
self endon( "death" );
self endon( "run_to_death" );
self set_ignore_enemy();
/#
self thread alien_ai_debug_print( "Lurker -_-zZ" );
#/
self thread wakeup_to_player_distance();
self thread wakeup_to_player_damage();
//self thread wakeup_to_enemy();
self thread walk_patrol_loop();
self waittill( "woke" );
self clear_ignore_enemy();
/#
self thread alien_ai_debug_print( "Lurker >:E" );
#/
wait 1; //wait for updates to agent combat variables
// run if is still walking
if ( self.movemode == "walk" )
self maps\mp\agents\alien\_alien_think::set_alien_movemode( "run" );
}
wakeup_to_enemy()
{
self endon( "woke" );
self endon( "death" );
self endon( "run_to_death" );
self waittill( "enemy" );
self notify( "woke" );
}
// force ignore and wake up, as ai_event dvars don't seem to work...
wakeup_to_player_distance( dist )
{
self endon( "woke" );
self endon( "death" );
self endon( "run_to_death" );
if ( !isdefined( dist ) )
dist = 512;
wakeup = false;
while ( !wakeup )
{
foreach ( player in get_players() )
{
if ( distance( player.origin, self.origin ) < dist )
{
wakeup = true;
break;
}
}
wait 0.25;
}
self notify( "woke" );
}
wakeup_to_player_damage()
{
self endon( "woke" );
self endon( "death" );
self endon( "run_to_death" );
while ( true )
{
self waittill( "damage", damage, attacker );
if ( isdefined( attacker ) && isalive( attacker ) && isplayer( attacker ) )
break;
}
self notify( "woke" );
}
walk_patrol_loop()
{
self endon( "woke" );
self endon( "death" );
self endon( "run_to_death" );
wait 1;
// if the patrol loop is setup we shall walk in circles!
if ( isdefined( level.patrol_start_nodes ) )
{
// get closest patrol path to walk on
node = getClosest( self.origin, level.patrol_start_nodes );
self maps\mp\agents\alien\_alien_think::set_alien_movemode( "walk" );
while ( true )
{
self ScrAgentSetGoalPos( node.origin );
self ScrAgentSetGoalRadius( 32 ); // try 64 if 32 is too small
self waittill( "goal_reached" );
// stops at a node
if ( IsDefined( node.script_delay ) )
wait node.script_delay;
node = Getstruct( node.target, "targetname" );
}
}
}
alien_ai_debug_print( nametag )
{
self endon ( "death" );
self notify ( "new_name_tag" );
self endon ( "new_name_tag" );
if ( getdvarint( "alien_debug_director" ) == 1 )
{
while ( true )
{
Print3d( self.origin, nametag, ( 1, 1, 1 ), 0.75, 2, 1 );
wait 0.05;
}
}
}
// ========================================================
// spawning aliens via meteoroid impact
// ========================================================
get_available_meteoroid_clip( node )
{
foreach ( clip in level.meteoroid_clips )
{
if ( !isdefined( clip.used_by ) )
{
clip.used_by = node;
return clip;
}
}
return undefined;
}
setup_meteoroid_paths()
{
level._effect[ "vfx_alien_lightning_bolt" ] = loadfx( "vfx/gameplay/alien/vfx_alien_lightning_bolt_02" );
level._effect[ "vfx_alien_lightning_impact" ] = loadfx( "vfx/gameplay/alien/vfx_alien_lightning_impact_debris_01" );
level.meteoroid_impact_nodes = [];
level.meteoroid_impact_nodes = getstructarray( "meteoroid_impact", "targetname" );
// default clips available for usage (3 max atm)
level.meteoroid_clips = [];
level.meteoroid_clips = GetEntArray( "meteoroid_clip", "targetname" );
foreach ( clip in level.meteoroid_clips )
{
clip.used_by = undefined;
clip.old_origin = clip.origin;
}
if ( !isdefined( level.meteoroid_impact_nodes ) || level.meteoroid_impact_nodes.size == 0 )
return;
foreach ( impact_node in level.meteoroid_impact_nodes )
{
impact_node.rocks = [];
impact_node.occupied = false;
targeted_array = GetStructArray( impact_node.target, "targetname" );
foreach ( targeted in targeted_array )
{
if ( !isdefined( targeted.script_noteworthy ) )
continue;
// rocks debris
if ( targeted.script_noteworthy == "rocks" )
{
impact_node.rocks[ impact_node.rocks.size ] = targeted;
}
// end position and model
if ( targeted.script_noteworthy == "meteoroid_final" )
{
impact_node.meteoroid_final_pos = targeted.origin;
impact_node.meteoroid_final_angles = targeted.angles;
impact_node.meteoroid = targeted;
// start position vector
start = getstruct( targeted.target, "targetname" );
if ( isdefined( start ) )
{
impact_node.meteoroid_start_pos = start.origin;
impact_node.meteoroid_start_angles = start.angles;
// end position vector, end is when it goes into the ground
end = getstruct( start.target, "targetname" );
if ( isdefined( end ) )
{
impact_node.meteoroid_end_pos = end.origin;
impact_node.meteoroid_end_angles = end.angles;
}
}
}
}
}
}
get_meteoroid_impact_node()
{
// escape sequence meteoroid impact logic
if ( flag_exist( "hives_cleared" ) && flag( "hives_cleared" ) )
return get_meteoroid_impact_node_escape();
// closest: to all players
CoM = get_center_of_players();
range = 2000; // only sample nodes 2000 units from center of players - local stronghold
free_nodes = [];
foreach ( node in level.meteoroid_impact_nodes )
{
// only non-escape meteor impact locations
if ( isdefined( node.script_noteworthy ) && ( node.script_noteworthy == "escape_blocker_meteor" || node.script_noteworthy == "escape_meteor" ) )
continue;
if ( Distance2D( node.origin, CoM ) > range )
continue;
if ( node.occupied == false )
free_nodes[ free_nodes.size ] = node;
}
if ( free_nodes.size > 0 )
return free_nodes[ randomint( free_nodes.size ) ]; // randomly return a location within 2000 units
else
return undefined;
}
get_meteoroid_impact_node_escape()
{
msg = "level.latest_choke_trig_active not defined during escape sequence";
assertex( isdefined( level.latest_choke_trig_active ) && isdefined( level.latest_choke_trig_active.choke_loc ), msg );
// closest: to chokes
CoM = level.latest_choke_trig_active.choke_loc;
range = 4000;
free_nodes = [];
foreach ( node in level.meteoroid_impact_nodes )
{
// only non-escape meteor impact locations
if ( !isdefined( node.script_noteworthy ) || ( node.script_noteworthy != "escape_blocker_meteor" && node.script_noteworthy != "escape_meteor" ) )
continue;
if ( Distance2D( node.origin, CoM ) > range )
continue;
if ( node.occupied == false )
free_nodes[ free_nodes.size ] = node;
}
if ( free_nodes.size > 0 )
{
closest_node = getClosest( CoM, free_nodes );
// if blocker node is the closest, we use it, else we randomly pick one
if ( isdefined( closest_node ) && isdefined( closest_node.script_noteworthy ) && closest_node.script_noteworthy == "escape_blocker_meteor" )
return closest_node;
else
return free_nodes[ randomint( free_nodes.size ) ];
}
else
{
return undefined;
}
}
get_center_of_players()
{
// closest: to all players
x = 0; y = 0; z = 0;
foreach ( player in level.players )
{
x += player.origin[ 0 ];
y += player.origin[ 1 ];
z += player.origin[ 2 ];
}
player_count = max( 1, level.players.size );
CoM = ( x/player_count, y/player_count, z/player_count ); // center of mass origin
return CoM;
}
CONST_ESCAPE_BLOCKER_METEOR_DELAY = 25; // seconds delay for blocker meteor
// spawn_alien_meteoroid( alien_type, count, respawn, spm, lasting_time )
// ===================================================
// alien_type = type reference string, ex: "wave cloaker"
// count (optional) = spawn this many aliens, respawn to maintain count (global) if respawn is on (default=4+)
// respawn (optional) = if true, count is number of aliens to maintain (default=false)
// spm (optional) = if respawn is on, spawns per minute, ex: 30 means it will spawn an alien per 2 seconds rate (default=60/minute)
// lasttime_time (optional) = time the meteoroid lasts for respawning (default=20sec)
spawn_alien_meteoroid( alien_type, count, respawn, spm, lasting_time )
{
level endon( "nuke_went_off" );
// spm = spawns per minute
if ( !isdefined( level.meteoroid_impact_nodes ) || level.meteoroid_impact_nodes.size == 0 )
return false;
if ( !isdefined( respawn ) )
respawn = false;
if ( !isdefined( count ) )
{
count = 4;
// two more for every existing meteoroid
foreach ( node in level.meteoroid_impact_nodes )
{
if ( node.occupied )
{
count += 2;
}
}
}
if ( !isdefined( spm ) )
spm = 60;
if ( !isdefined( lasting_time ) )
lasting_time = 20;
impact_node = get_meteoroid_impact_node();
if ( !isdefined( impact_node ) )
{
//IPrintLnBold( "All meteoroid impact locations are occupied or too far" );
return false;
}
clip = get_available_meteoroid_clip( impact_node );
if ( !isdefined( clip ) )
{
//IPrintLnBold( "All meteoroid impact locations are occupied or too far" );
return false;
}
impact_node.occupied = true;
impact_node.meteoroid.ent = Spawn( "script_model", impact_node.meteoroid.origin );
impact_node.meteoroid.ent setmodel( "mp_ext_alien_meteor" );
start_pos = impact_node.meteoroid_start_pos;
start_angles = impact_node.meteoroid_start_angles;
final_pos = impact_node.meteoroid_final_pos;
final_angles = impact_node.meteoroid_final_angles;
end_pos = impact_node.meteoroid_end_pos;
end_angles = impact_node.meteoroid_end_angles;
travel_time = 5;
accel = 4;
lightning_strikes = 3;
// faster in escape sequence
if ( flag_exist( "hives_cleared" ) && flag( "hives_cleared" ) )
{
travel_time = 3;
accel = 1;
lightning_strikes = 2;
}
impact_node.meteoroid.ent.origin = start_pos;
impact_node.meteoroid.ent.angles = start_angles;
level thread maps\mp\alien\_music_and_dialog::playVOForMeteor();
impact_node.meteoroid.ent MoveTo( final_pos, travel_time, accel );
thread playSoundInSpace( "alien_minion_spawn_mtr_incoming", final_pos );
//impact_node.meteoroid.ent RotateTo( final_angles, travel_time, accel );
impact_node.meteoroid.ent RotateVelocity( (0,360,0), travel_time, accel );
for ( i=0; i<lightning_strikes; i++ )
{
// lightning strikes!
PlayFx( level._effect[ "vfx_alien_lightning_bolt" ], final_pos );
thread playSoundInSpace( "alien_minion_spawn_lightning", final_pos );
wait travel_time/lightning_strikes;
}
impact_node.meteoroid.ent.origin = final_pos;
impact_node.meteoroid.ent.angles = final_angles;
/*
// show rocks
foreach ( rock in impact_node.rocks )
{
rock.ent = Spawn( "script_model", rock.origin );
rock.ent setmodel( "moab_river_rock_cluster_04" ); // JL: moab_river_rock_cluster_04 no longer in csv, rocks do not match awesome new meteor model
rock.ent.angles = rock.angles;
}
*/
// playfx
PlayFx( level._effect[ "vfx_alien_lightning_impact" ], impact_node.origin );
Earthquake( 0.75, 1, impact_node.origin, 2000 );
thread playSoundInSpace( "alien_meteor_impact", impact_node.origin );
PlayRumbleOnPosition( "grenade_rumble", impact_node.origin );
RadiusDamage( impact_node.origin, 256, 150, 10 );
// bring on the clippage
clip.origin = impact_node.origin;
clip DisconnectPaths();
// lightning strikes!
PlayFx( level._effect[ "vfx_alien_lightning_bolt" ], final_pos );
// spawn minions!
spawn_meteoroid_aliens( impact_node, alien_type, count, respawn, spm, lasting_time );
if ( flag_exist( "hives_cleared" )
&& flag( "hives_cleared" )
&& isdefined( impact_node.script_noteworthy )
&& impact_node.script_noteworthy == "escape_blocker_meteor" )
{
total_delay = CONST_ESCAPE_BLOCKER_METEOR_DELAY; // delay introduced to slow down players
lightning_strikes = 3;
for ( i=0; i<lightning_strikes; i++ )
{
// lightning strikes!
PlayFx( level._effect[ "vfx_alien_lightning_bolt" ], final_pos );
thread playSoundInSpace( "alien_minion_spawn_lightning", final_pos );
wait total_delay/lightning_strikes;
}
}
// reset
PlayFx( level._effect[ "vfx_alien_lightning_bolt" ], final_pos );
thread playSoundInSpace( "alien_minion_spawn_lightning", final_pos );
impact_node.meteoroid.ent MoveTo( end_pos, travel_time/2, accel/2 );
//impact_node.meteoroid.ent RotateTo( end_angles, travel_time/2, accel/2 );
impact_node.meteoroid.ent RotateVelocity( (0,90,0), travel_time/2, accel/2 );
// play fx
PlayFX( level._effect["queen_ground_spawn"], impact_node.origin, (0,0,1) );
//PlayFX( level._effect[ "stronghold_explode_large" ], impact_node.origin );
thread playSoundInSpace( "alien_minion_spawn_mtr_sink", impact_node.origin );
Earthquake( 0.3, travel_time/2, impact_node.origin, 512 );
wait travel_time/4;
// lightning strikes AGAIN!
//PlayFX( level._effect["queen_ground_spawn"], impact_node.origin, (0,0,1) );
//PlayFx( level._effect[ "vfx_alien_lightning_bolt" ], final_pos ); PlayFx( level._effect[ "vfx_alien_lightning_bolt" ], final_pos );
wait travel_time/4;
impact_node.meteoroid.ent delete();
/*
foreach ( rock in impact_node.rocks )
rock.ent delete();
*/
// remove the clippage
clip.origin = clip.old_origin;
clip ConnectPaths();
clip.used_by = undefined;
impact_node.occupied = false;
}
spawn_meteoroid_aliens( impact_node, alien_type, count, respawn, spm, spawn_time )
{
level endon( "nuke_went_off" );
pos = impact_node.origin;
wait 1;
spawned = 0;
end_time = GetTime() + spawn_time * 1000.0;
spawned_aliens = [];
while ( 1 )
{
if ( GetTime() >= end_time )
{
level notify( "meteor_aliens_spawned", spawned_aliens, count );
return;
}
if ( spawned >= count && !respawn )
{
level notify( "meteor_aliens_spawned", spawned_aliens, count );
// kill it
wait 7;
return;
}
if ( !can_spawn_meteoroid_alien( alien_type, count ) )
{
wait 0.05;
continue;
}
CoM = get_center_of_players();
direction_vec = VectorNormalize( CoM - pos ); // facing center of players
direction_vec = RotateVector( direction_vec, ( 0, 120 - randomint( 120 ), 0 ) );
spawner_angles = VectorToAngles( direction_vec );
agent = spawnAlien( pos, spawner_angles, "wave " + alien_type );
// only crawl out nonblocking meteor
if ( !isdefined( impact_node.script_noteworthy ) || ( impact_node.script_noteworthy != "escape_blocker_meteor" && impact_node.script_noteworthy != "escape_meteor" ) )
agent thread crawl_out( pos, direction_vec );
spawned_aliens[spawned_aliens.size] = agent;
spawned++;
// offset from other meteoroids if there are more than one
wait RandomFloatRange( 0.05, 0.25 );
wait ( 60/spm ) - 0.15;
}
}
can_spawn_meteoroid_alien( alien_type, count )
{
alive_by_type = 0;
foreach ( agent in level.agentArray )
{
if ( isalive( agent ) )
{
if ( agent is_alien_agent() && agent.alien_type == alien_type )
alive_by_type++;
}
}
budgetUsed = maps\mp\alien\_spawn_director::get_current_agent_count( true );
// budget - 1
if ( budgetUsed > 17 )
{
return false;
}
if ( alive_by_type > count )
{
return false;
}
return true;
}
crawl_out( pos, direction_vec )
{
self endon( "death" );
self enable_alien_scripted();
default_rate = self.moveplaybackrate;
self.moveplaybackrate = 0.5;
self ScrAgentSetGoalRadius( 4000 );
self ScrAgentSetAnimMode( "anim deltas" );
self ScrAgentSetOrientMode( "face angle abs", self.angles );
self ScrAgentSetPhysicsMode( "noclip" );
// climb out
self SetOrigin( pos + VectorNormalize( direction_vec ) * 64 );
self moveToEndOnGround( "traverse_climb_up", 4 );
self SetAnimState( "traverse_climb_up", 4 );
anim_length = getAnimLength( self GetAnimEntry( "traverse_climb_up", 4 )); // animation time
wait anim_length/2;
self ScrAgentSetPhysicsMode( "gravity" );
wait anim_length/2;
self.moveplaybackrate = default_rate;
self clear_ignore_enemy();
//TEMP: force walk
original_health = self.health;
original_moverate = self.moveplaybackrate;
original_animrate = self.animplaybackrate;
while ( self.health > original_health * 0.95 )
{
self maps\mp\agents\alien\_alien_think::set_alien_movemode( "walk" );
self.moveplaybackrate = 1.75;
self.animplaybackrate = 1.75;
wait 0.05;
}
self maps\mp\agents\alien\_alien_think::set_alien_movemode( "run" );
self.moveplaybackrate = original_moverate;
self.animplaybackrate = original_animrate;
}
moveToEndOnGround( animState, animIndex, animState2, animIndex2 )
{
VERTICAL_DELTA_BUFFER = 2;
GET_GROUND_DROP_HEIGHT = 60;
AI_PHYSICS_TRACE_RADIUS = 32;
anime = self GetAnimEntry( animState, animIndex );
animLength = GetAnimLength( anime );
animDelta = GetMoveDelta( anime, 0, 1 );
offsetFromStart = RotateVector( animDelta, self.angles ); // vector translation of the animation
trace_start_pos = self.origin + ( offsetFromStart[0], offsetFromStart[1], GET_GROUND_DROP_HEIGHT );
trace_end_pos = trace_start_pos - ( 0, 0, 2*GET_GROUND_DROP_HEIGHT );
ground_pos = self AIPhysicsTrace( trace_start_pos, trace_end_pos, AI_PHYSICS_TRACE_RADIUS, 65 );
lerp_target_pos = ground_pos - offsetFromStart + ( 0, 0, VERTICAL_DELTA_BUFFER );
self SetOrigin( lerp_target_pos );
// play fx
PlayFX( level._effect["drone_ground_spawn"], ( self.origin[ 0 ], self.origin[ 1 ], ground_pos[ 2 ] ), (0,0,1) );
debug_line( trace_start_pos, ground_pos, (1,0,0), 30 );
debug_origin( ground_pos, 4, (1,0,0), 30 );
debug_origin( self.origin+offsetFromStart, 4, (1,1,1), 30 );
debug_line( self.origin, self.origin+offsetFromStart, (1,1,1), 30 );
}
debug_origin( vector, size, color, frames )
{
/#
size = size/2;
thread draw_line( vector+(0,0,size), vector-(0,0,size), color, frames);
thread draw_line( vector+(0,size,0), vector-(0,size,0), color, frames);
thread draw_line( vector+(size,0,0), vector-(size,0,0), color, frames);
#/
}
debug_line( from, to, color, frames )
{
/#
thread draw_line( from, to, color, frames);
#/
}
draw_line( from, to, color, frames )
{
if ( GetDvarInt( "alien_debug_director" ) > 0 )
{
//level endon( "helicopter_done" );
if( isdefined( frames ) )
{
for( i=0; i<frames; i++ )
{
line( from, to, color );
wait 0.05;
}
}
else
{
for( ;; )
{
line( from, to, color );
wait 0.05;
}
}
}
}
// ========================================================
// SPAWN HELPER FUNCTIONS
// ========================================================
// ----- reference data -----
// struct []: returns array of cycles
get_cycles( stronghold )
{
return level.strongholds[ stronghold ].cycles;
}
// bool: returns of cycle is repeated at this stronghold
is_cycle_repeated( stronghold, cycle )
{
cycles = get_cycles( stronghold );
return cycles[ cycle ].repeat;
}
// struct []: returns array of waves
get_waves( stronghold, cycle )
{
cycles = get_cycles( stronghold );
return cycles[ cycle ].waves;
}
// ----- current data -----
// int: returns current cycle
get_current_cycle()
{
return level.alien_wave_status[ "cycle" ];
}
// int: returns current wave
get_current_wave()
{
return level.alien_wave_status[ "wave" ];
}
use_spawn_director()
{
if ( isDefined ( level.use_spawn_director ) && level.use_spawn_director == 1)
return true;
return false;
}
encounter_cycle_spawn( force_cycle_start_notify, endon_notify )
{
level endon( "game_ended" );
level.current_cycle_started_by_timeout = undefined;
if ( isDefined( endon_notify ) )
level endon( endon_notify );
if ( !isDefined( level.cycle_count ) )
level.cycle_count = init_cycle_count();
cycle_spawn_delay = get_cycle_spawn_delay();
msg = undefined;
if ( isDefined( force_cycle_start_notify ) )
msg = level waittill_any_timeout( cycle_spawn_delay, force_cycle_start_notify );
else
wait cycle_spawn_delay;
if ( isDefined( msg ) && msg == "timeout" )
level.current_cycle_started_by_timeout = true;
cycle = level.cycle_count;
if ( isDefined( level.get_custom_cycle_func ) )
cycle = [[level.get_custom_cycle_func]]();
maps\mp\alien\_spawn_director::start_cycle( cycle );
level.cycle_count++;
/#
if ( getdvarint ( "scr_debugcyclecount" ) == 1)
{
println( "CYCLE_COUNT_DEBUG -- > Cycle Count INCREMENTED to: " + level.cycle_count );
}
#/
}
init_cycle_count()
{
/#
if ( GetDvarInt( "scr_startingcycle" ) > 0 )
return GetDvarInt( "scr_startingcycle" );
#/
return 1;
}
get_cycle_spawn_delay()
{
extra_time = get_extra_spawn_delay();
return ( level.cycle_data.cycle_delay_times[ level.cycle_count ] + extra_time );
}
get_extra_spawn_delay()
{
if ( is_chaos_mode() )
return 0;
else
return ( getNumActiveAgents() * 3.0 );
}