2236 lines
64 KiB
Plaintext
2236 lines
64 KiB
Plaintext
// ----------------------------------------------------------------------------
|
|
// #using
|
|
// ----------------------------------------------------------------------------
|
|
#using scripts\codescripts\struct;
|
|
|
|
#using scripts\shared\gameskill_shared;
|
|
#using scripts\shared\math_shared;
|
|
#using scripts\shared\statemachine_shared;
|
|
#using scripts\shared\system_shared;
|
|
#using scripts\shared\array_shared;
|
|
#using scripts\shared\util_shared;
|
|
#using scripts\shared\clientfield_shared;
|
|
|
|
#using scripts\shared\ai_shared;
|
|
#using scripts\shared\vehicle_shared;
|
|
#using scripts\shared\vehicle_death_shared;
|
|
#using scripts\shared\vehicle_ai_shared;
|
|
|
|
#using scripts\shared\ai\systems\blackboard;
|
|
#using scripts\shared\ai\blackboard_vehicle;
|
|
|
|
#using scripts\shared\turret_shared;
|
|
#using scripts\shared\weapons\_spike_charge_siegebot;
|
|
|
|
#using scripts\mp\vehicles\_siegebot;
|
|
#using scripts\shared\callbacks_shared;
|
|
#using scripts\shared\laststand_shared;
|
|
|
|
#using scripts\shared\util_shared;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#using scripts\mp\killstreaks\_killstreaks;
|
|
#using scripts\mp\killstreaks\_killstreak_bundles;
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// #define
|
|
// ----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// #namespace
|
|
// ----------------------------------------------------------------------------
|
|
#namespace siegebot_theia;
|
|
|
|
function autoexec __init__sytem__() { system::register("siegebot_theia",&__init__,undefined,undefined); }
|
|
|
|
#using_animtree( "generic" );
|
|
|
|
function __init__()
|
|
{
|
|
vehicle::add_main_callback( "siegebot_theia", &siegebot_initialize );
|
|
|
|
clientfield::register( "vehicle", "sarah_rumble_on_landing", 1, 1, "counter" );
|
|
clientfield::register( "vehicle", "sarah_minigun_spin", 1, 1, "int" );
|
|
}
|
|
|
|
function siegebot_initialize()
|
|
{
|
|
self useanimtree( #animtree );
|
|
|
|
blackboard::CreateBlackBoardForEntity( self );
|
|
self Blackboard::RegisterVehicleBlackBoardAttributes();
|
|
|
|
self.health = self.healthdefault;
|
|
|
|
self vehicle::friendly_fire_shield();
|
|
|
|
self EnableAimAssist();
|
|
self SetNearGoalNotifyDist( self.radius * 1.2 );
|
|
|
|
Target_Set( self, ( 0, 0, 150 ) );
|
|
|
|
self.fovcosine = 0; // +/-90 degrees = 180 fov, err 0 actually means 360 degree view
|
|
self.fovcosinebusy = 0;
|
|
self.maxsightdistsqrd = ( (10000) * (10000) );
|
|
|
|
assert( isdefined( self.scriptbundlesettings ) );
|
|
|
|
self.settings = struct::get_script_bundle( "vehiclecustomsettings", self.scriptbundlesettings );
|
|
|
|
self.goalRadius = 9999999;
|
|
self.goalHeight = 5000;
|
|
self SetGoal( self.origin, false, self.goalRadius, self.goalHeight );
|
|
|
|
self.overrideVehicleDamage = &theia_callback_damage;
|
|
|
|
self pain_toggle( true );
|
|
|
|
//util::magic_bullet_shield( self ); // we will disable death for the entire battle, until health get below threshold and theia get to designated location, then we grant her the privilege to die
|
|
|
|
if( !SessionModeIsMultiplayerGame() )
|
|
self initJumpStruct();
|
|
|
|
self SetGunnerTurretOnTargetRange( 0, self.settings.gunner_turret_on_target_range );
|
|
|
|
self locomotion_start();
|
|
|
|
self thread init_clientfields();
|
|
|
|
self.damageLevel = 0;
|
|
self.newDamageLevel = self.damageLevel;
|
|
|
|
self init_player_threat_all();
|
|
self init_fake_targets();
|
|
|
|
if ( isdefined( self.combat_goal_volume ) )
|
|
{
|
|
self SetGoal( self.combat_goal_volume );
|
|
}
|
|
|
|
if ( !isdefined( self.height ) )
|
|
{
|
|
self.height = self.radius;
|
|
}
|
|
|
|
self.noCybercom = true;
|
|
self.ignoreFireFly = true;
|
|
self.ignoreDecoy = true;
|
|
self vehicle_ai::InitThreatBias();
|
|
|
|
killstreak_bundles::register_killstreak_bundle( "siegebot_theia" );
|
|
self.maxhealth = killstreak_bundles::get_max_health( "siegebot_theia" );
|
|
self.heatlh = self.maxhealth;
|
|
}
|
|
|
|
function init_clientfields()
|
|
{
|
|
self vehicle::lights_on();
|
|
self vehicle::toggle_lights_group( 1, true );
|
|
self vehicle::toggle_lights_group( 2, true );
|
|
self vehicle::toggle_lights_group( 3, true );
|
|
self clientfield::set( "sarah_minigun_spin", 0 );
|
|
}
|
|
|
|
function defaultRole()
|
|
{
|
|
self vehicle_ai::init_state_machine_for_role();
|
|
|
|
self vehicle_ai::get_state_callbacks( "combat" ).enter_func = &state_balconyCombat_enter;
|
|
self vehicle_ai::get_state_callbacks( "combat" ).update_func = &state_balconyCombat_update;
|
|
self vehicle_ai::get_state_callbacks( "combat" ).exit_func = &state_balconyCombat_exit;
|
|
|
|
self vehicle_ai::get_state_callbacks( "pain" ).enter_func = &pain_enter;
|
|
self vehicle_ai::get_state_callbacks( "pain" ).update_func = &pain_update;
|
|
self vehicle_ai::get_state_callbacks( "pain" ).exit_func = &pain_exit;
|
|
|
|
self vehicle_ai::get_state_callbacks( "scripted" ).exit_func = &scripted_exit;
|
|
|
|
self vehicle_ai::get_state_callbacks( "death" ).update_func = &state_death_update;
|
|
|
|
self vehicle_ai::add_state( "jumpUp",
|
|
&state_jumpUp_enter,
|
|
&state_jump_update,
|
|
&state_jump_exit );
|
|
|
|
self vehicle_ai::add_state( "jumpDown",
|
|
&state_jumpDown_enter,
|
|
&state_jump_update,
|
|
&state_jumpDown_exit );
|
|
|
|
self vehicle_ai::add_state( "jumpGroundToGround",
|
|
&state_jumpDown_enter,
|
|
&state_jump_update,
|
|
&state_jump_exit );
|
|
|
|
// this is the normal siegebot ground combat
|
|
self vehicle_ai::add_state( "groundCombat",
|
|
undefined,
|
|
&state_groundCombat_update,
|
|
&state_groundCombat_exit );
|
|
|
|
self vehicle_ai::add_state( "prepareDeath",
|
|
undefined,
|
|
&prepare_death_update,
|
|
undefined );
|
|
|
|
vehicle_ai::add_interrupt_connection( "groundCombat", "pain", "pain" );
|
|
|
|
vehicle_ai::add_utility_connection( "emped", "groundCombat" );
|
|
vehicle_ai::add_utility_connection( "pain", "groundCombat" );
|
|
|
|
vehicle_ai::add_utility_connection( "combat", "jumpDown", &can_jump_down );
|
|
vehicle_ai::add_utility_connection( "jumpDown", "groundCombat" );
|
|
|
|
vehicle_ai::add_utility_connection( "groundCombat", "jumpGroundToGround", &can_jump_ground_to_ground );
|
|
vehicle_ai::add_utility_connection( "jumpGroundToGround", "groundCombat" );
|
|
|
|
vehicle_ai::add_utility_connection( "groundCombat", "jumpUp", &can_jump_up );
|
|
vehicle_ai::add_utility_connection( "jumpUp", "combat" );
|
|
|
|
vehicle_ai::add_utility_connection( "groundCombat", "prepareDeath", &should_prepare_death );
|
|
|
|
vehicle_ai::Cooldown( "jump", 11 * 2 );
|
|
vehicle_ai::Cooldown( "jumpUp", 11 * 3 );
|
|
vehicle_ai::StartInitialState( "groundCombat" );
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: death
|
|
// ----------------------------------------------
|
|
function state_death_update( params )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "nodeath_thread" );
|
|
|
|
self SetTurretSpinning( false );
|
|
self clean_up_spawned();
|
|
|
|
self stopMovementAndSetBrake();
|
|
self SetTurretTargetRelativeAngles( (0,0,0) ); // Reset the turret angles so the animation lines up
|
|
|
|
self vehicle_death::death_fx();
|
|
self vehicle_death::set_death_model( self.deathmodel, self.modelswapdelay );
|
|
self vehicle::set_damage_fx_level( 0 );
|
|
self playsound("veh_quadtank_sparks");
|
|
}
|
|
|
|
function clean_up_spawned()
|
|
{
|
|
if ( isdefined( self.jump ) )
|
|
{
|
|
self.jump.linkEnt Delete();
|
|
}
|
|
|
|
if ( isdefined( self.fakeTargetEnt ) )
|
|
{
|
|
self.fakeTargetEnt Delete();
|
|
}
|
|
|
|
if ( isdefined( self.spikeFakeTargets ) )
|
|
{
|
|
foreach( target in self.spikeFakeTargets )
|
|
{
|
|
target Delete();
|
|
}
|
|
}
|
|
}
|
|
// State: death ----------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: pain
|
|
// ----------------------------------------------
|
|
function pain_toggle( enabled )
|
|
{
|
|
self._enablePain = enabled;
|
|
}
|
|
|
|
function pain_canenter()
|
|
{
|
|
state = vehicle_ai::get_current_state();
|
|
return isdefined( state ) && state != "pain" && self._enablePain;
|
|
}
|
|
|
|
function pain_enter( params )
|
|
{
|
|
self stopMovementAndSetBrake();
|
|
}
|
|
|
|
function pain_exit( params )
|
|
{
|
|
self SetBrake( 0 );
|
|
}
|
|
|
|
function pain_update( params )
|
|
{
|
|
self endon( "death" );
|
|
|
|
if ( 1 <= self.damagelevel && self.damagelevel <= 4 )
|
|
{
|
|
asmState = "damage_" + self.damageLevel + "@pain";
|
|
}
|
|
else
|
|
{
|
|
asmState = "normal@pain";
|
|
}
|
|
|
|
self ASMRequestSubstate( asmState );
|
|
self vehicle_ai::waittill_asm_complete( asmState, 5 );
|
|
|
|
vehicle_ai::AddCooldownTime( "jump", -11 * 0.4 );
|
|
vehicle_ai::AddCooldownTime( "jumpUp", -11 );
|
|
|
|
previous_state = vehicle_ai::get_previous_state();
|
|
self vehicle_ai::set_state( previous_state );
|
|
self vehicle_ai::evaluate_connections();
|
|
}
|
|
// State: pain ----------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: prepare_death
|
|
// ----------------------------------------------
|
|
function should_prepare_death( from_state, to_state, connection )
|
|
{
|
|
prepare_death_threshold = self.healthdefault * 0.1;
|
|
if ( self.health < prepare_death_threshold )
|
|
{
|
|
return 99999999; // big number so we are guarenteed to take this state
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function prepare_death_update( params )
|
|
{
|
|
self endon ( "death" );
|
|
self endon( "change_state" );
|
|
|
|
// don't shoot spike immediately
|
|
vehicle_ai::Cooldown( "spike_on_ground", 2 );
|
|
self thread Attack_Thread_Gun();
|
|
self thread Attack_Thread_Rocket();
|
|
locomotion_start();
|
|
|
|
startTime = GetTime();
|
|
while ( Distance2DSquared( self.origin, self.death_goal_point ) > 1200 && vehicle_ai::TimeSince( startTime ) < 8 )
|
|
{
|
|
self SetVehGoalPos( self.death_goal_point, false, true );
|
|
self SetBrake( 0 );
|
|
wait 1;
|
|
}
|
|
|
|
self CancelAIMove();
|
|
self ClearVehGoalPos();
|
|
self SetBrake( 1 );
|
|
|
|
self notify( "end_attack_thread" );
|
|
self notify( "end_movement_thread" );
|
|
self.jump.highground_history = self.jump.highgrounds[0];
|
|
self state_jumpUp_enter( params );
|
|
self state_jump_update( params );
|
|
|
|
// now she is allowed to die
|
|
//util::stop_magic_bullet_shield( self );
|
|
self.disable_side_step = true;
|
|
self state_balconyCombat_update( params );
|
|
}
|
|
// State: prepare_death -------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: scripted
|
|
// ----------------------------------------------
|
|
function scripted_exit( params )
|
|
{
|
|
vehicle_ai::Cooldown( "jump", 11 * 2 );
|
|
vehicle_ai::Cooldown( "jumpUp", 11 * 3 );
|
|
}
|
|
// State: scripted ------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: jump
|
|
// ----------------------------------------------
|
|
function initJumpStruct()
|
|
{
|
|
if ( isdefined( self.jump ) )
|
|
{
|
|
self Unlink();
|
|
self.jump.linkEnt Delete();
|
|
self.jump Delete();
|
|
}
|
|
|
|
self.jump = spawnstruct();
|
|
self.jump.linkEnt = Spawn( "script_origin", self.origin );
|
|
self.jump.in_air = false;
|
|
self.jump.highgrounds = struct::get_array( "balcony_point" );
|
|
self.jump.groundpoints = struct::get_array( "ground_point" );
|
|
self.arena_center = struct::get( "arena_center" ).origin;
|
|
self.death_goal_point = struct::get( "death_goal_point" ).origin;
|
|
self.combat_goal_volume = GetEnt( "theia_combat_region", "targetname" );
|
|
|
|
foreach( point in self.jump.highgrounds )
|
|
{
|
|
// fixing point -24566.2 23972.5 -20000
|
|
if ( DistanceSquared( point.origin, (-24566.2, 23972.5, -20000) ) < ( (100) * (100) ) )
|
|
{
|
|
point.origin += (20, -20, -100);
|
|
}
|
|
// fixing point -27291.2 25825.6 -20072
|
|
else if ( DistanceSquared( point.origin, (-27291.2, 25825.6, -20072) ) < ( (100) * (100) ) )
|
|
{
|
|
point.origin += (0, 35, 0);
|
|
}
|
|
}
|
|
|
|
assert( self.jump.highgrounds.size > 0 );
|
|
assert( self.jump.groundpoints.size > 0 );
|
|
assert( isdefined( self.arena_center ) );
|
|
}
|
|
|
|
function can_jump_up( from_state, to_state, connection )
|
|
{
|
|
if ( !vehicle_ai::IsCooldownReady( "jump" ) || !vehicle_ai::IsCooldownReady( "jumpUp" ) )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
target = highGroundPoint( 800, 2000, self.jump.highgrounds, 1200 );
|
|
|
|
if ( isdefined( target ) )
|
|
{
|
|
self.jump.highground_history = target;
|
|
return 500;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function state_jumpUp_enter( params )
|
|
{
|
|
goal = self.jump.highground_history.origin;
|
|
|
|
trace = PhysicsTrace( goal + ( 0, 0, 200 ), goal - ( 0, 0, 10000 ), ( -10, -10, -10 ), ( 10, 10, 10 ), self, (1 << 1) );
|
|
if ( false )
|
|
{
|
|
/#debugstar( goal, 60000, (0,1,0) ); #/
|
|
/#debugstar( trace[ "position" ], 60000, (0,1,0) ); #/
|
|
/#line(goal, trace[ "position" ], (0,1,0), 1, false, 60000 ); #/
|
|
}
|
|
if ( trace[ "fraction" ] < 1 )
|
|
{
|
|
goal = trace[ "position" ];
|
|
}
|
|
|
|
self.jump.highground_history = goal;
|
|
self.jump.goal = goal;
|
|
|
|
params.scaleForward = 70;
|
|
params.gravityForce = (0, 0, -5);
|
|
params.upByHeight = 10;
|
|
params.landingState = "land_turn@jump";
|
|
|
|
self pain_toggle( false );
|
|
|
|
self stopMovementAndSetBrake();
|
|
}
|
|
|
|
function can_jump_down( from_state, to_state, connection )
|
|
{
|
|
if ( !vehicle_ai::IsCooldownReady( "jump" ) || self.dontchangestate === true )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
target = get_jumpon_target( 800, 2000, 1300 );
|
|
|
|
if ( isdefined( target ) )
|
|
{
|
|
self.jump.lowground_history = target;
|
|
return 500;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function state_jumpDown_enter( params )
|
|
{
|
|
goal = self.jump.lowground_history;
|
|
|
|
trace = PhysicsTrace( goal + ( 0, 0, 500 ), goal - ( 0, 0, 10000 ), ( -10, -10, -10 ), ( 10, 10, 10 ), self, (1 << 1) );
|
|
if ( false )
|
|
{
|
|
/#debugstar( goal, 60000, (0,1,0) ); #/
|
|
/#debugstar( trace[ "position" ], 60000, (0,1,0) ); #/
|
|
/#line(goal, trace[ "position" ], (0,1,0), 1, false, 60000 ); #/
|
|
}
|
|
if ( trace[ "fraction" ] < 1 )
|
|
{
|
|
goal = trace[ "position" ];
|
|
}
|
|
|
|
self.jump.lowground_history = goal;
|
|
self.jump.goal = goal;
|
|
|
|
params.scaleForward = 70;
|
|
params.gravityForce = (0, 0, -5);
|
|
params.upByHeight = -5;
|
|
params.landingState = "land@jump";
|
|
|
|
self pain_toggle( false );
|
|
|
|
self stopMovementAndSetBrake();
|
|
}
|
|
|
|
function can_jump_ground_to_ground( from_state, to_state, connection )
|
|
{
|
|
if ( !vehicle_ai::IsCooldownReady( "jump" ) )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
target = get_jumpon_target( 800, 1800, 1300, false, 0, false );
|
|
|
|
if ( isdefined( target ) )
|
|
{
|
|
self.jump.lowground_history = target;
|
|
return 400;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function state_jump_exit( params )
|
|
{
|
|
self pain_toggle( true );
|
|
}
|
|
|
|
function state_jumpDown_exit( params )
|
|
{
|
|
self pain_toggle( true );
|
|
self vehicle_ai::Cooldown( "jumpUp", 11 + randomFloatRange( -1, 3 ) );
|
|
}
|
|
|
|
function state_jump_update( params )
|
|
{
|
|
self endon( "change_state" );
|
|
self endon( "death" );
|
|
|
|
goal = self.jump.goal;
|
|
|
|
self face_target( goal );
|
|
|
|
self.jump.linkEnt.origin = self.origin;
|
|
self.jump.linkEnt.angles = self.angles;
|
|
|
|
{wait(.05);};
|
|
|
|
self LinkTo( self.jump.linkEnt );
|
|
|
|
self.jump.in_air = true;
|
|
|
|
if ( false )
|
|
{
|
|
/#debugstar( goal, 60000, (0,1,0) ); #/
|
|
/#debugstar( goal + (0,0,100), 60000, (0,1,0) ); #/
|
|
/#line(goal, goal + (0,0,100), (0,1,0), 1, false, 60000 ); #/
|
|
}
|
|
|
|
// calculate distance and forces
|
|
totalDistance = Distance2D(goal, self.jump.linkEnt.origin);
|
|
forward = ( ((goal - self.jump.linkEnt.origin) / totalDistance)[0], ((goal - self.jump.linkEnt.origin) / totalDistance)[1], 0 );
|
|
upByDistance = MapFloat( 500, 2000, 46, 52, totalDistance );
|
|
antiGravityByDistance = MapFloat( 500, 2000, 0, 0.5, totalDistance );
|
|
|
|
initVelocityUp = (0,0,1) * ( upByDistance + params.upByHeight );
|
|
initVelocityForward = forward * params.scaleForward * MapFloat( 500, 2000, 0.8, 1, totalDistance );
|
|
velocity = initVelocityUp + initVelocityForward;
|
|
|
|
// start jumping
|
|
self ASMRequestSubstate( "inair@jump" );
|
|
self waittill( "engine_startup" );
|
|
self vehicle::impact_fx( self.settings.startupfx1 );
|
|
self waittill( "leave_ground" );
|
|
self vehicle::impact_fx( self.settings.takeofffx1 );
|
|
|
|
jumpStart = GetTime();
|
|
while( true )
|
|
{
|
|
distanceToGoal = Distance2D(self.jump.linkEnt.origin, goal);
|
|
|
|
antiGravityScaleUp = MapFloat( 0, 0.5, 0.6, 0, abs( 0.5 - distanceToGoal / totalDistance ) );
|
|
antiGravityScale = MapFloat( (self.radius * 1.0), (self.radius * 3), 0, 1, distanceToGoal );
|
|
antiGravity = antiGravityScale * antiGravityScaleUp * (-params.gravityForce) + (0,0,antiGravityByDistance);
|
|
if ( false ) /#line(self.jump.linkEnt.origin, self.jump.linkEnt.origin + antiGravity, (0,1,0), 1, false, 60000 ); #/
|
|
|
|
velocityForwardScale = MapFloat( (self.radius * 1), (self.radius * 4), 0.2, 1, distanceToGoal );
|
|
velocityForward = initVelocityForward * velocityForwardScale;
|
|
if ( false ) /#line(self.jump.linkEnt.origin, self.jump.linkEnt.origin + velocityForward, (0,1,0), 1, false, 60000 ); #/
|
|
|
|
oldVerticleSpeed = velocity[2];
|
|
velocity = (0,0, velocity[2]);
|
|
velocity += velocityForward + params.gravityForce + antiGravity;
|
|
|
|
if ( oldVerticleSpeed > 0 && velocity[2] <= 0 )
|
|
{
|
|
self ASMRequestSubstate( "fall@jump" );
|
|
}
|
|
|
|
if ( ( velocity[2] <= 0 && self.jump.linkEnt.origin[2] + velocity[2] <= goal[2] ) || vehicle_ai::TimeSince( jumpStart ) > 10 )
|
|
{
|
|
break;
|
|
}
|
|
|
|
heightThreshold = goal[2] + 110;
|
|
oldHeight = self.jump.linkEnt.origin[2];
|
|
self.jump.linkEnt.origin += velocity;
|
|
|
|
if ( self.jump.linkEnt.origin[2] < heightThreshold && ( oldHeight > heightThreshold || ( oldVerticleSpeed > 0 && velocity[2] < 0 ) ) )
|
|
{
|
|
self notify( "start_landing" );
|
|
self ASMRequestSubstate( params.landingState );
|
|
}
|
|
|
|
if ( false ) /#debugstar( self.jump.linkEnt.origin, 60000, (1,0,0) ); #/
|
|
{wait(.05);};
|
|
}
|
|
|
|
// landed
|
|
self.jump.linkEnt.origin = ( self.jump.linkEnt.origin[0], self.jump.linkEnt.origin[1], 0 ) + ( 0, 0, goal[2] );
|
|
self notify( "land_crush" );
|
|
|
|
// don't damage player, but crush player vehicle
|
|
foreach( player in level.players )
|
|
{
|
|
player._takedamage_old = player.takedamage;
|
|
player.takedamage = false;
|
|
}
|
|
self RadiusDamage( self.origin + ( 0,0,15 ), self.radiusdamageradius, self.radiusdamagemax, self.radiusdamagemin, self, "MOD_EXPLOSIVE" );
|
|
|
|
foreach( player in level.players )
|
|
{
|
|
player.takedamage = player._takedamage_old;
|
|
player._takedamage_old = undefined;
|
|
|
|
if ( Distance2DSquared( self.origin, player.origin ) < ( (200) * (200) ) )
|
|
{
|
|
direction = ( ( player.origin - self.origin )[0], ( player.origin - self.origin )[1], 0 );
|
|
if ( Abs( direction[0] ) < 0.01 && Abs( direction[1] ) < 0.01 )
|
|
{
|
|
direction = ( RandomFloatRange( 1, 2 ), RandomFloatRange( 1, 2 ), 0 );
|
|
}
|
|
direction = VectorNormalize( direction );
|
|
strength = 700;
|
|
player SetVelocity( player GetVelocity() + direction * strength );
|
|
|
|
if ( player.health > 80 )
|
|
{
|
|
player DoDamage( player.health - 70, self.origin, self );
|
|
}
|
|
else
|
|
{
|
|
player DoDamage( 20, self.origin, self );
|
|
}
|
|
}
|
|
}
|
|
|
|
self vehicle::impact_fx( self.settings.landingfx1 );
|
|
self stopMovementAndSetBrake();
|
|
|
|
//rumble for landing from jump
|
|
self clientfield::increment( "sarah_rumble_on_landing" );
|
|
|
|
wait 0.3;
|
|
|
|
self Unlink();
|
|
|
|
{wait(.05);};
|
|
|
|
self.jump.in_air = false;
|
|
|
|
self notify ( "jump_finished" );
|
|
|
|
vehicle_ai::Cooldown( "jump", 11 );
|
|
vehicle_ai::Cooldown( "ignore_player", 12 );
|
|
|
|
self vehicle_ai::waittill_asm_complete( params.landingState, 3 );
|
|
|
|
self vehicle_ai::evaluate_connections();
|
|
}
|
|
// State: jump ----------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: combat
|
|
// ----------------------------------------------
|
|
function state_balconyCombat_enter( params )
|
|
{
|
|
self vehicle_ai::ClearAllLookingAndTargeting();
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 0 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 1 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 2 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 3 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 4 );
|
|
}
|
|
|
|
function state_balconyCombat_update( params )
|
|
{
|
|
self endon( "change_state" );
|
|
self endon( "death" );
|
|
|
|
// face the correct direction
|
|
currentHighGround = undefined;
|
|
foreach( highGround in self.jump.highgrounds )
|
|
{
|
|
if ( distance2DSquared( highGround.origin, self.origin ) < ( (self.radius * 6) * (self.radius * 6) ) )
|
|
{
|
|
currentHighGround = highGround;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !isdefined( currentHighGround ) )
|
|
{
|
|
self vehicle_ai::ClearCooldown( "jump" );
|
|
self vehicle_ai::evaluate_connections();
|
|
}
|
|
|
|
forward = anglesToForward( currentHighGround.angles );
|
|
|
|
while ( true )
|
|
{
|
|
while ( !isdefined(self.enemy) )
|
|
{
|
|
wait 1;
|
|
}
|
|
|
|
self face_target( self.origin + forward * 10000 );
|
|
|
|
javelinChance = self.damageLevel * 0.15;
|
|
if ( randomFloat( 1.0 ) < javelinChance )
|
|
{
|
|
attack_javelin();
|
|
|
|
level notify( "theia_finished_platform_attack" );
|
|
self vehicle_ai::evaluate_connections();
|
|
wait 0.8;
|
|
}
|
|
|
|
attack_spike_minefield();
|
|
|
|
level notify( "theia_finished_platform_attack" );
|
|
self vehicle_ai::evaluate_connections();
|
|
|
|
if ( RandomFloat( 1 ) > 0.4 && self.disable_side_step !== true )
|
|
{
|
|
wait 0.2;
|
|
self side_step();
|
|
}
|
|
wait 0.8;
|
|
|
|
attack_minigun_sweep();
|
|
|
|
level notify( "theia_finished_platform_attack" );
|
|
self vehicle_ai::evaluate_connections();
|
|
wait 0.8;
|
|
}
|
|
}
|
|
|
|
function side_step()
|
|
{
|
|
step_size = 180; // trace length, not the actual move length. need to match with animation
|
|
|
|
right_dir = AnglesToRight( self.angles );
|
|
start = self.origin + (0,0,10);
|
|
|
|
traceDir = right_dir;
|
|
jukeState = "juke_r@movement";
|
|
oppositeJukeState = "juke_l@movement";
|
|
|
|
if ( math::cointoss() )
|
|
{
|
|
traceDir = -traceDir;
|
|
jukeState = "juke_l@movement";
|
|
oppositeJukeState = "juke_r@movement";
|
|
}
|
|
|
|
trace = PhysicsTrace( start, start + traceDir * step_size, 0.8 * ( -self.radius, -self.radius, 0 ), 0.8 * ( self.radius, self.radius, self.height ), self, (1 << 1) );
|
|
|
|
if ( false )
|
|
{
|
|
/#line(start, start + traceDir * step_size, (1,0,0), 1, false, 100 ); #/
|
|
}
|
|
|
|
if ( trace["fraction"] < 1 )
|
|
{
|
|
traceDir = -traceDir;
|
|
trace = PhysicsTrace( start, start + traceDir * step_size, 0.8 * ( -self.radius, -self.radius, 0 ), 0.8 * ( self.radius, self.radius, self.height ), self, (1 << 1) );
|
|
jukeState = oppositeJukeState;
|
|
if ( false )
|
|
{
|
|
/#line(start, start + traceDir * step_size, (1,0,0), 1, false, 100 ); #/
|
|
}
|
|
}
|
|
|
|
if ( trace["fraction"] >= 1 )
|
|
{
|
|
self ASMRequestSubstate( jukeState );
|
|
self vehicle_ai::waittill_asm_complete( jukeState, 3 );
|
|
self locomotion_start();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function state_balconyCombat_exit( params )
|
|
{
|
|
self vehicle_ai::ClearAllLookingAndTargeting();
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 0 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 1 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 2 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 3 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 4 );
|
|
}
|
|
// State: combat ----------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: groundCombat
|
|
// ----------------------------------------------
|
|
function state_groundCombat_update( params )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
|
|
// don't shoot spike immediately
|
|
if( vehicle_ai::get_previous_state() === "jump" )
|
|
{
|
|
vehicle_ai::Cooldown( "spike_on_ground", 2 );
|
|
}
|
|
|
|
self thread Attack_Thread_Gun();
|
|
self thread Attack_Thread_Rocket();
|
|
self thread Movement_Thread();
|
|
self thread footstep_left_monitor();
|
|
self thread footstep_right_monitor();
|
|
|
|
while ( true )
|
|
{
|
|
self vehicle_ai::evaluate_connections();
|
|
wait 1;
|
|
}
|
|
}
|
|
|
|
function footstep_damage( tag_name )
|
|
{
|
|
origin = self GetTagOrigin( tag_name );
|
|
|
|
// don't damage player, but crush player vehicle
|
|
foreach( player in level.players )
|
|
{
|
|
player._takedamage_old = player.takedamage;
|
|
player.takedamage = false;
|
|
}
|
|
self RadiusDamage( origin + ( 0,0,10 ), self.radius, 200, 200, self, "MOD_EXPLOSIVE" );
|
|
|
|
foreach( player in level.players )
|
|
{
|
|
player.takedamage = player._takedamage_old;
|
|
player._takedamage_old = undefined;
|
|
|
|
if ( Distance2DSquared( origin, player.origin ) < ( (self.radius) * (self.radius) ) )
|
|
{
|
|
player DoDamage( 15, origin, self );
|
|
}
|
|
}
|
|
}
|
|
|
|
function footstep_left_monitor()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self notify( "stop_left_footstep_damage" );
|
|
self endon( "stop_left_footstep_damage" );
|
|
|
|
while ( true )
|
|
{
|
|
self waittill( "footstep_left_large_theia" );
|
|
footstep_damage( "tag_leg_left_foot_animate" );
|
|
}
|
|
}
|
|
|
|
function footstep_right_monitor()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self notify( "stop_right_footstep_damage" );
|
|
self endon( "stop_right_footstep_damage" );
|
|
|
|
while ( true )
|
|
{
|
|
self waittill( "footstep_right_large_theia" );
|
|
footstep_damage( "tag_leg_right_foot_animate" );
|
|
}
|
|
}
|
|
|
|
function highGroundPoint( distanceLimitMin, distanceLimitMax, pointsArray, idealDist )
|
|
{
|
|
/# Record3DText( "range: [" + distanceLimitMin + "," + distanceLimitMax + "]", self.origin, (1,0.5,0), "Script", self ); #/
|
|
|
|
bestScore = 1000000; // lower the better
|
|
result = undefined;
|
|
foreach( point in pointsArray )
|
|
{
|
|
distanceToTarget = Distance2D( point.origin, self.origin );
|
|
if ( distanceToTarget < distanceLimitMin || distanceLimitMax < distanceToTarget )
|
|
{
|
|
/# RecordStar( point.origin, (1,0.5,0) ); #/
|
|
/# Record3DText( "out of range: " + distanceToTarget, point.origin, (1,0.5,0), "Script", self ); #/
|
|
continue;
|
|
}
|
|
|
|
score = Abs( distanceToTarget - idealDist );
|
|
if ( score < 200 )
|
|
{
|
|
score = randomFloat( 200 );
|
|
}
|
|
|
|
if ( isdefined( self.jump.highground_history ) && Distance2DSquared( point.origin, self.jump.highground_history ) < ( (50) * (50) ) )
|
|
{
|
|
score += 1000;
|
|
}
|
|
|
|
/# RecordStar( point.origin, (1,0.5,0) ); #/
|
|
/# Record3DText( "dist: " + distanceToTarget + " score: " + score, point.origin, (1,0.5,0), "Script", self ); #/
|
|
|
|
if ( score < bestScore )
|
|
{
|
|
bestScore = score;
|
|
result = point;
|
|
}
|
|
}
|
|
|
|
if ( isdefined( result ) )
|
|
{
|
|
return result;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function state_groundCombat_exit( params )
|
|
{
|
|
self notify( "end_attack_thread" );
|
|
self notify( "end_movement_thread" );
|
|
self ClearTurretTarget();
|
|
self SetTurretSpinning( false );
|
|
}
|
|
|
|
function get_player_vehicle( player )
|
|
{
|
|
if ( isPlayer( player ) )
|
|
{
|
|
if ( player.usingvehicle && isdefined( player.viewlockedentity ) && isVehicle( player.viewlockedentity ) )
|
|
{
|
|
return player.viewlockedentity;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function get_player_and_vehicle_array()
|
|
{
|
|
targets = level.players;
|
|
|
|
vehicles = [];
|
|
foreach ( player in level.players )
|
|
{
|
|
vehicle = get_player_vehicle( player );
|
|
if ( isdefined( vehicle ) )
|
|
{
|
|
if ( !isdefined( vehicles ) ) vehicles = []; else if ( !IsArray( vehicles ) ) vehicles = array( vehicles ); vehicles[vehicles.size]=vehicle;;
|
|
}
|
|
}
|
|
|
|
targets = ArrayCombine( targets, vehicles, false, false );
|
|
return targets;
|
|
}
|
|
|
|
function init_player_threat( player )
|
|
{
|
|
index = player GetEntityNumber();
|
|
|
|
if ( !isdefined( self.player_threat ) )
|
|
{
|
|
self.player_threat = [];
|
|
|
|
for( i = 0; i < 4; i++ )
|
|
{
|
|
self.player_threat[self.player_threat.size] = SpawnStruct();
|
|
}
|
|
}
|
|
|
|
if ( !isdefined( self.player_threat[index].damage ) ||
|
|
!isdefined( self.player_threat[index].tempBoost ) ||
|
|
!isdefined( self.player_threat[index].tempBoostTimeout ) )
|
|
{
|
|
reset_player_threat( player );
|
|
}
|
|
}
|
|
|
|
// self == vehicle
|
|
function init_player_threat_all()
|
|
{
|
|
callback::on_spawned( &init_player_threat, self );
|
|
callback::on_player_killed( &init_player_threat, self );
|
|
callback::on_laststand( &init_player_threat, self );
|
|
|
|
foreach( player in level.players )
|
|
{
|
|
self init_player_threat( player );
|
|
}
|
|
}
|
|
|
|
// self == vehicle
|
|
function reset_player_threat( player )
|
|
{
|
|
index = player GetEntityNumber();
|
|
|
|
// find out other player's minDamage. this is to prevent hot join player never getting picked as target because damage factor is 0.
|
|
minDamage = self.player_threat[index].damage;
|
|
if ( !isdefined( minDamage ) )
|
|
{
|
|
minDamage = 1000000;
|
|
}
|
|
|
|
if ( self.player_threat.size > 0 )
|
|
{
|
|
foreach( threat in self.player_threat )
|
|
{
|
|
if ( isdefined( threat.damage ) )
|
|
{
|
|
minDamage = min( minDamage, threat.damage );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
minDamage = 0;
|
|
}
|
|
|
|
self.player_threat[index].damage = minDamage;
|
|
self.player_threat[index].tempBoost = 0;
|
|
self.player_threat[index].tempBoostTimeout = 0;
|
|
}
|
|
|
|
// self == vehicle
|
|
function add_player_threat_damage( player, damage )
|
|
{
|
|
index = player GetEntityNumber();
|
|
self.player_threat[index].damage += damage;
|
|
}
|
|
|
|
// self == vehicle
|
|
function add_player_threat_boost( player, boost, timeSeconds )
|
|
{
|
|
index = player GetEntityNumber();
|
|
|
|
if ( self.player_threat[index].tempBoostTimeout <= GetTime() )
|
|
{
|
|
self.player_threat[index].tempBoost = 0;
|
|
}
|
|
|
|
self.player_threat[index].tempBoost += boost;
|
|
self.player_threat[index].tempBoostTimeout = GetTime() + timeSeconds * 1000;
|
|
}
|
|
|
|
// self == vehicle
|
|
function get_player_threat( player )
|
|
{
|
|
if ( !is_valid_target( player ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
timeIgnoreOnSpawn = 7; //seconds
|
|
currentTime = GetTime();
|
|
if ( isdefined( player._spawn_time ) && ( player._spawn_time + timeIgnoreOnSpawn * 1000 > currentTime ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
index = player GetEntityNumber();
|
|
|
|
if ( !isdefined( self.player_threat ) || !isdefined( self.player_threat[ index ] ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
threat = self.player_threat[index].damage;
|
|
|
|
if ( self.player_threat[index].tempBoostTimeout > GetTime() )
|
|
{
|
|
threat += self.player_threat[index].tempBoost;
|
|
}
|
|
|
|
if ( self.main_target === player )
|
|
{
|
|
threat += 1000;
|
|
}
|
|
|
|
if( self VehSeenRecently( player, 3 ) )
|
|
{
|
|
threat += 1000;
|
|
}
|
|
|
|
if ( player.health < 50 )
|
|
{
|
|
threat -= 800;
|
|
}
|
|
|
|
distanceSqr = Distance2DSquared( self.origin, player.origin );
|
|
if ( distanceSqr < ( (800) * (800) ) )
|
|
{
|
|
threat += 800;
|
|
}
|
|
else if ( distanceSqr < ( (1500) * (1500) ) )
|
|
{
|
|
threat += 400;
|
|
}
|
|
|
|
return threat;
|
|
}
|
|
|
|
// self == vehicle
|
|
function update_target_player()
|
|
{
|
|
best_threat = -1000000;
|
|
self.main_target = undefined;
|
|
foreach( player in level.players )
|
|
{
|
|
threat = get_player_threat( player );
|
|
if ( isdefined( threat ) && threat > best_threat )
|
|
{
|
|
best_threat = threat;
|
|
self.main_target = player;
|
|
}
|
|
}
|
|
}
|
|
|
|
function shoulder_light_focus( target )
|
|
{
|
|
if ( !isdefined( target ) )
|
|
{
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 3 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 4 );
|
|
}
|
|
else
|
|
{
|
|
self vehicle_ai::SetTurretTarget( target, 3 );
|
|
self vehicle_ai::SetTurretTarget( target, 4 );
|
|
}
|
|
}
|
|
|
|
function Debug_line_to_target( target, time, color )
|
|
{
|
|
self endon( "death" );
|
|
point1 = self.origin;
|
|
point2 = target.origin;
|
|
if ( false )
|
|
{
|
|
stopTime = GetTime() + time * 1000;
|
|
while ( GetTime() <= stopTime )
|
|
{
|
|
/#line(point1, point2, color, 1, false, 3 ); #/
|
|
{wait(.05);};
|
|
}
|
|
}
|
|
}
|
|
|
|
function Pin_first_three_spikes_to_ground( delay )
|
|
{
|
|
self endon( "death" );
|
|
|
|
wait delay;
|
|
for( i = 0; i < 3 && i < self.spikeFakeTargets.size; i++ )
|
|
{
|
|
spike = self.spikeFakeTargets[ i ];
|
|
spike pin_to_ground();
|
|
wait 0.15;
|
|
}
|
|
}
|
|
|
|
function Attack_Thread_Gun()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self endon( "end_attack_thread" );
|
|
self notify( "end_attack_thread_gun" );
|
|
self endon( "end_attack_thread_gun" );
|
|
|
|
while( 1 )
|
|
{
|
|
enemy = self.enemy;
|
|
if( !isdefined( enemy ) )
|
|
{
|
|
self SetTurretTargetRelativeAngles( (0,0,0) );
|
|
wait 0.4;
|
|
continue;
|
|
}
|
|
|
|
if( !enemy.allowdeath && !IsPlayer(enemy) )
|
|
{
|
|
self SetPersonalThreatBias( enemy, -2000, 8.0 );
|
|
wait 0.4;
|
|
continue;
|
|
}
|
|
|
|
distSq = DistanceSquared( enemy.origin, self.origin );
|
|
if ( self VehCanSee( enemy ) && ( IsPlayer( enemy ) || ( ( (200) * (200) ) < distSq && distSq < ( (2000) * (2000) ) ) ) ) // don't shoot enemy that's not in good range unless it's player
|
|
{
|
|
self SetPersonalThreatBias( enemy, 1000, 1.0 );
|
|
}
|
|
else
|
|
{
|
|
self SetPersonalThreatBias( enemy, -1000, 1.0 );
|
|
}
|
|
|
|
self vehicle_ai::SetTurretTarget( enemy, 0 );
|
|
self vehicle_ai::SetTurretTarget( enemy, 1 );
|
|
self shoulder_light_focus( enemy );
|
|
|
|
gun_on_target = GetTime();
|
|
self SetTurretSpinning( true );
|
|
while( isdefined( enemy ) && !self.gunner1ontarget && vehicle_ai::TimeSince( gun_on_target ) < 2 )
|
|
{
|
|
wait 0.4;
|
|
}
|
|
|
|
if( !isdefined( enemy ) )
|
|
{
|
|
self SetTurretSpinning( false );
|
|
continue;
|
|
}
|
|
|
|
attack_start = GetTime();
|
|
while ( isdefined( enemy ) && enemy === self.enemy && self VehSeenRecently( enemy, 1.0 ) && vehicle_ai::TimeSince( attack_start ) < 5 )
|
|
{
|
|
self vehicle_ai::fire_for_time( 1.0 + RandomFloat( 0.4 ), 1 );
|
|
|
|
if ( isdefined( enemy ) && isPlayer( enemy ) )
|
|
{
|
|
wait( 0.6 + RandomFloat( 0.2 ) );
|
|
}
|
|
wait 0.1;
|
|
}
|
|
|
|
self SetTurretSpinning( false );
|
|
|
|
wait 0.1; // avoid infinite loop
|
|
}
|
|
}
|
|
|
|
function Attack_Thread_Rocket()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self endon( "end_attack_thread" );
|
|
self notify( "end_attack_thread_rocket" );
|
|
self endon( "end_attack_thread_rocket" );
|
|
|
|
while( 1 )
|
|
{
|
|
enemy = self.enemy;
|
|
if( !isdefined( enemy ) )
|
|
{
|
|
wait 0.4;
|
|
continue;
|
|
}
|
|
|
|
if ( vehicle_ai::IsCooldownReady( "spike_on_ground", 2 ) && self.rocketaim !== true )
|
|
{
|
|
self toggle_rocketaim( true );
|
|
}
|
|
|
|
if ( !vehicle_ai::IsCooldownReady( "spike_on_ground" ) )
|
|
{
|
|
wait 0.4;
|
|
continue;
|
|
}
|
|
|
|
// select a secondary enemy
|
|
primaryEnemy = enemy;
|
|
|
|
targets = GetAITeamArray( "allies" );
|
|
targets = ArrayCombine( targets, level.players, false, false );
|
|
|
|
dirToPrimaryEnemy = VectorNormalize( ( (primaryEnemy.origin - self.origin)[0], (primaryEnemy.origin - self.origin)[1], 0 ) );
|
|
|
|
bestCloseScore = 0.0;
|
|
bestTarget = undefined;
|
|
foreach( target in targets )
|
|
{
|
|
if ( target IsNoTarget() || target == primaryEnemy )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
dirToTarget = VectorNormalize( ( (target.origin - self.origin)[0], (target.origin - self.origin)[1], 0 ) );
|
|
angleDot = VectorDot( dirToTarget, dirToPrimaryEnemy );
|
|
if ( angleDot < 0.2 )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
distanceSelfToTargetSqr = Distance2DSquared( target.origin, self.origin );
|
|
if ( distanceSelfToTargetSqr < ( (400) * (400) ) || distanceSelfToTargetSqr > ( (1200) * (1200) ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
closeTargetScore = spike_score( target );
|
|
|
|
closeTargetScore += 1 - angleDot;
|
|
|
|
if ( isPlayer( target ) )
|
|
{
|
|
closeTargetScore += 0.5;
|
|
}
|
|
|
|
distancePrimaryEnemyToTargetSqr = Distance2DSquared( target.origin, primaryEnemy.origin );
|
|
if ( distancePrimaryEnemyToTargetSqr < ( (200) * (200) ) )
|
|
{
|
|
closeTargetScore -= 0.3;
|
|
}
|
|
|
|
if ( bestCloseScore <= closeTargetScore )
|
|
{
|
|
bestCloseScore = closeTargetScore;
|
|
bestTarget = target;
|
|
}
|
|
}
|
|
|
|
enemy = bestTarget;
|
|
|
|
if ( isAlive( enemy ) )
|
|
{
|
|
if ( false )
|
|
{
|
|
self thread Debug_line_to_target( enemy, 5, (1,0,0) );
|
|
}
|
|
|
|
turretOrigin = self GetTagOrigin( "tag_gunner_flash2" );
|
|
distToEnemy = Distance2D( self.origin, enemy.origin );
|
|
shootHeight = math::clamp( distToEnemy * 0.35, 100, 350 );
|
|
points = GeneratePointsAroundCenter( enemy.origin + (0,0,shootHeight), 300, 80, 50 );
|
|
pinDelay = Mapfloat( 300, 700, 0.1, 1.0, distToEnemy );
|
|
|
|
// get the turret angle looking correct
|
|
spike = self.spikeFakeTargets[ 0 ];
|
|
spike.origin = points[ 0 ];
|
|
self SetGunnerTargetEnt( spike, (0,0,0), 1 );
|
|
|
|
rocket_on_target = GetTime();
|
|
while( !self.gunner2ontarget && vehicle_ai::TimeSince( rocket_on_target ) < 2 )
|
|
{
|
|
wait 0.4;
|
|
}
|
|
|
|
self thread Pin_first_three_spikes_to_ground( pinDelay );
|
|
|
|
for( i = 0; i < 3 && i < self.spikeFakeTargets.size && i < points.size; i++ )
|
|
{
|
|
spike = self.spikeFakeTargets[ i ];
|
|
spike.origin = points[ i ];
|
|
self SetGunnerTargetEnt( spike, (0,0,0), 1 );
|
|
self FireWeapon( 2, enemy );
|
|
vehicle_ai::Cooldown( "spike_on_ground", randomFloatRange( 6, 10 ) );
|
|
|
|
if ( false )
|
|
{
|
|
/#debugstar( spike.origin, 200, (1,0,0) ); #/
|
|
/#Circle( spike.origin, 150, (1,0,0), false, true, 200 ); #/
|
|
}
|
|
|
|
wait 0.1;
|
|
}
|
|
|
|
wait 0.5;
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 2 );
|
|
self toggle_rocketaim( false );
|
|
}
|
|
else
|
|
{
|
|
wait 0.4;
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggle_rocketaim( is_aiming )
|
|
{
|
|
self.rocketaim = is_aiming;
|
|
self locomotion_start();
|
|
}
|
|
|
|
function locomotion_start()
|
|
{
|
|
if ( self.rocketaim === true )
|
|
{
|
|
locomotion = "locomotion@movement";
|
|
}
|
|
else
|
|
{
|
|
locomotion = "locomotion_rocketup@movement";
|
|
}
|
|
|
|
self ASMRequestSubstate( locomotion );
|
|
}
|
|
|
|
function Get_Strong_Target()
|
|
{
|
|
minDist = 400;
|
|
|
|
ai_array = GetAITeamArray( "allies" );
|
|
ai_array = array::randomize( ai_array );
|
|
foreach( ai in ai_array )
|
|
{
|
|
awayFromPlayer = true;
|
|
foreach( player in level.players )
|
|
{
|
|
if ( is_valid_target( player ) && Distance2DSquared( ai.origin, player.origin ) < ( (minDist) * (minDist) ) )
|
|
{
|
|
awayFromPlayer = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !awayFromPlayer )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function Movement_Thread()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self notify( "end_movement_thread" );
|
|
self endon( "end_movement_thread" );
|
|
|
|
while( true )
|
|
{
|
|
// try to get player as target
|
|
self update_target_player();
|
|
enemy = self.main_target;
|
|
|
|
// if there is only one player, ignore player once every once in a while
|
|
if ( level.players.size <= 1 && vehicle_ai::IsCooldownReady( "ignore_player" ) )
|
|
{
|
|
vehicle_ai::Cooldown( "ignore_player", 12 );
|
|
enemy = Get_Strong_Target();
|
|
foreach( player in level.players )
|
|
{
|
|
self SetPersonalThreatBias( player, -1000, 2.0 );
|
|
}
|
|
}
|
|
|
|
// fallback to general enemy
|
|
if ( !isdefined(enemy) )
|
|
{
|
|
enemy = self.enemy;
|
|
}
|
|
|
|
// no enemy, just don't move
|
|
if ( !isdefined(enemy) )
|
|
{
|
|
{wait(.05);};
|
|
continue;
|
|
}
|
|
|
|
self.current_pathto_pos = self GetNextMovePosition( enemy );
|
|
self.current_enemy_pos = enemy.origin;
|
|
|
|
self SetSpeed( self.settings.defaultMoveSpeed );
|
|
|
|
foundpath = self SetVehGoalPos( self.current_pathto_pos, false, true );
|
|
if ( foundPath )
|
|
{
|
|
self SetLookAtEnt( enemy );
|
|
self SetBrake( 0 );
|
|
locomotion_start();
|
|
self thread path_update_interrupt();
|
|
self vehicle_ai::waittill_pathing_done();
|
|
self notify( "end_path_interrupt" );
|
|
self CancelAIMove();
|
|
self ClearVehGoalPos();
|
|
self SetBrake( 1 );
|
|
}
|
|
|
|
{wait(.05);};
|
|
}
|
|
}
|
|
|
|
function path_update_interrupt()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self endon( "end_movement_thread" );
|
|
self notify( "end_path_interrupt" );
|
|
self endon( "end_path_interrupt" );
|
|
|
|
while( true )
|
|
{
|
|
if ( isdefined( self.current_enemy_pos ) && isdefined( self.main_target ) )
|
|
{
|
|
if ( Distance2DSquared( self.current_enemy_pos, self.main_target.origin ) > ( (200) * (200) ) )
|
|
{
|
|
self notify( "near_goal" );
|
|
}
|
|
}
|
|
wait 0.8;
|
|
}
|
|
}
|
|
|
|
function GetNextMovePosition( enemy )
|
|
{
|
|
if( self.goalforced )
|
|
{
|
|
return self.goalpos;
|
|
}
|
|
|
|
halfHeight = 400;
|
|
spacing = 80;
|
|
queryOrigin = self.origin;
|
|
|
|
if ( isdefined( enemy ) && self CanPath( self.origin, enemy.origin ) )
|
|
{
|
|
queryOrigin = enemy.origin;
|
|
}
|
|
|
|
queryResult = PositionQuery_Source_Navigation( queryOrigin, 0, self.settings.engagementDistMax + 200, halfHeight, spacing, self );
|
|
|
|
if ( isdefined( enemy ) )
|
|
{
|
|
PositionQuery_Filter_Sight( queryResult, enemy.origin, self GetEye() - self.origin, self, 0, enemy );
|
|
vehicle_ai::PositionQuery_Filter_EngagementDist( queryResult, enemy, self.settings.engagementDistMin, self.settings.engagementDistMax );
|
|
}
|
|
PositionQuery_Filter_DistanceToGoal( queryResult, self );
|
|
vehicle_ai::PositionQuery_Filter_OutOfGoalAnchor( queryResult );
|
|
|
|
forward = AnglesToForward( self.angles );
|
|
if ( isdefined( enemy ) )
|
|
{
|
|
enemyDir = VectorNormalize( enemy.origin - self.origin );
|
|
forward = VectorNormalize( forward + 5 * enemyDir );
|
|
}
|
|
|
|
foreach ( point in queryResult.data )
|
|
{
|
|
if( Distance2DSquared( self.origin, point.origin ) < ( (300) * (300) ) )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "tooCloseToSelf" ] = -700; #/ point.score += -700;;
|
|
}
|
|
|
|
if( isdefined( enemy ) )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "engagementDist" ] = -point.distAwayFromEngagementArea; #/ point.score += -point.distAwayFromEngagementArea;;
|
|
|
|
if ( !point.visibility )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "visibility" ] = -600; #/ point.score += -600;;
|
|
}
|
|
}
|
|
|
|
pointDirection = VectorNormalize( point.origin - self.origin );
|
|
factor = VectorDot( pointDirection, forward );
|
|
if ( factor > 0.7 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "directionDiff" ] = 600; #/ point.score += 600;;
|
|
}
|
|
else if ( factor > 0 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "directionDiff" ] = 0; #/ point.score += 0;;
|
|
}
|
|
else if ( factor > -0.5 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "directionDiff" ] = -600; #/ point.score += -600;;
|
|
}
|
|
else
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "directionDiff" ] = -1200; #/ point.score += -1200;;
|
|
}
|
|
}
|
|
|
|
vehicle_ai::PositionQuery_PostProcess_SortScore( queryResult );
|
|
self vehicle_ai::PositionQuery_DebugScores( queryResult );
|
|
|
|
if( queryResult.data.size == 0 )
|
|
return self.origin;
|
|
|
|
return queryResult.data[0].origin;
|
|
}
|
|
// State: groundCombat ----------------------------------
|
|
|
|
function _sort_by_distance2d( left, right, point )
|
|
{
|
|
distanceSqrToLeft = distance2DSquared( left.origin, point );
|
|
distanceSqrToRight = distance2DSquared( right.origin, point );
|
|
return distanceSqrToLeft > distanceSqrToRight;
|
|
}
|
|
|
|
function too_close_to_high_ground( point, minDistance )
|
|
{
|
|
foreach( highGround in self.jump.highgrounds )
|
|
{
|
|
if ( Distance2DSquared( point, highGround.origin ) < ( (minDistance) * (minDistance) ) )
|
|
{
|
|
return true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function get_jumpon_target( distanceLimitMin, distanceLimitMax, idealDist, includingAI, minAngleDiffCos, mustJump )
|
|
{
|
|
targets = level.players;
|
|
|
|
if ( includingAI === true )
|
|
{
|
|
targets = ArrayCombine( targets, GetAITeamArray( "allies" ), false, false );
|
|
targets = array::merge_sort( targets, &_sort_by_distance2d, self.origin );
|
|
}
|
|
|
|
angles = ( 0, self.angles[1], 0 );
|
|
|
|
forward = AnglesToForward( angles );
|
|
|
|
bestTarget = undefined;
|
|
bestScore = 1000000; // lower the better
|
|
|
|
minDistAwayFromHighGround = 300;
|
|
maxDistAwayFromArenaCenter = 1800;
|
|
|
|
/# RecordStar( self.origin, (1,0.5,0) ); #/
|
|
/# Record3DText( "JUMP TO GROUND", self.origin, (1,0.5,0), "Script", self ); #/
|
|
|
|
foreach( target in targets )
|
|
{
|
|
if ( !is_valid_target( target ) || !target.allowdeath || IsAirBorne( target ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( Distance2DSquared( self.arena_center, target.origin ) > ( (maxDistAwayFromArenaCenter) * (maxDistAwayFromArenaCenter) ) )
|
|
{
|
|
/# RecordStar( target.origin, (0,0.5,1) ); #/
|
|
/# Record3DText( "too far from center: " + distance2d( self.arena_center, target.origin ), target.origin, (0,0.5,1), "Script", self ); #/
|
|
continue;
|
|
}
|
|
|
|
if ( too_close_to_high_ground( target.origin, minDistAwayFromHighGround ) )
|
|
{
|
|
/# RecordStar( target.origin, (0,0.5,1) ); #/
|
|
/# Record3DText( "too close to platform", target.origin, (0,0.5,1), "Script", self ); #/
|
|
continue;
|
|
}
|
|
|
|
distanceToTarget = Distance2D( target.origin, self.origin );
|
|
if ( distanceToTarget < distanceLimitMin || distanceLimitMax < distanceToTarget )
|
|
{
|
|
/# RecordStar( target.origin, (1,0.5,0) ); #/
|
|
/# Record3DText( "out of range: " + distanceToTarget, target.origin, (1,0.5,0), "Script", self ); #/
|
|
continue;
|
|
}
|
|
|
|
vectorToTarget = ( ( target.origin - self.origin )[0], ( target.origin - self.origin )[1], 0 );
|
|
vectorToTarget = vectorToTarget / distanceToTarget;
|
|
if ( isdefined( minAngleDiffCos ) && VectorDot( forward, vectorToTarget ) < minAngleDiffCos )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
score = Abs( distanceToTarget - idealDist );
|
|
if ( score < 200 )
|
|
{
|
|
score = randomFloat( 200 );
|
|
}
|
|
|
|
/# RecordStar( target.origin, (1,0.5,0) ); #/
|
|
/# Record3DText( "dist: " + distanceToTarget + " score: " + score, target.origin, (1,0.5,0), "Script", self ); #/
|
|
|
|
if ( isPlayer( target ) && !isVehicle( target ) )
|
|
{
|
|
minRadius = 0;
|
|
maxRadius = 300;
|
|
}
|
|
else
|
|
{
|
|
minRadius = 200;
|
|
maxRadius = 400;
|
|
}
|
|
queryResult = PositionQuery_Source_Navigation( target.origin, minRadius, maxRadius, 500, self.radius * 0.5, self.radius * 1.1 );
|
|
if ( queryResult.data.size > 0 )
|
|
{
|
|
element = queryResult.data[0];
|
|
if ( score < bestScore )
|
|
{
|
|
bestScore = score;
|
|
bestTarget = element;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( isdefined( bestTarget ) )
|
|
{
|
|
return bestTarget.origin;
|
|
}
|
|
|
|
if ( mustJump === false )
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
// pick random point using arena_center
|
|
queryResult = PositionQuery_Source_Navigation( self.arena_center, 100, 1300, 500, self.radius, self.radius * 1.1 );
|
|
|
|
assert ( queryResult.data.size > 0 );
|
|
pointList = array::randomize( queryResult.data );
|
|
foreach ( point in pointList )
|
|
{
|
|
distanceToTargetSqr = Distance2DSquared( point.origin, self.origin );
|
|
if ( ( (distanceLimitMin) * (distanceLimitMin) ) < distanceToTargetSqr && distanceToTargetSqr < ( (distanceLimitMax) * (distanceLimitMax) ) && !too_close_to_high_ground( point.origin, minDistAwayFromHighGround ) )
|
|
{
|
|
return point.origin;
|
|
}
|
|
}
|
|
|
|
return self.arena_center;
|
|
}
|
|
|
|
function stopMovementAndSetBrake()
|
|
{
|
|
self notify( "end_movement_thread" );
|
|
self notify( "near_goal" );
|
|
self CancelAIMove();
|
|
self ClearVehGoalPos();
|
|
self ClearTurretTarget();
|
|
self ClearLookAtEnt();
|
|
self SetBrake( 1 );
|
|
}
|
|
|
|
function face_target( position, targetAngleDiff )
|
|
{
|
|
if ( !isdefined( targetAngleDiff ) )
|
|
{
|
|
targetAngleDiff = 30;
|
|
}
|
|
|
|
v_to_enemy = ( (position - self.origin)[0], (position - self.origin)[1], 0 );
|
|
v_to_enemy = VectorNormalize( v_to_enemy );
|
|
goalAngles = VectortoAngles( v_to_enemy );
|
|
|
|
angleDiff = AbsAngleClamp180( self.angles[1] - goalAngles[1] );
|
|
if ( angleDiff <= targetAngleDiff )
|
|
{
|
|
return;
|
|
}
|
|
|
|
self SetLookAtOrigin( position );
|
|
self SetTurretTargetVec( position );
|
|
self locomotion_start();
|
|
|
|
angleAdjustingStart = GetTime();
|
|
while( angleDiff > targetAngleDiff && vehicle_ai::TimeSince( angleAdjustingStart ) < 4 )
|
|
{
|
|
if ( false ) /#line(self.origin, position, (1,0,1), 1, false, 5 ); #/
|
|
angleDiff = AbsAngleClamp180( self.angles[1] - goalAngles[1] );
|
|
{wait(.05);};
|
|
}
|
|
|
|
self ClearVehGoalPos();
|
|
self ClearLookAtEnt();
|
|
self ClearTurretTarget();
|
|
self CancelAIMove();
|
|
}
|
|
|
|
function theia_callback_damage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, weapon, vPoint, vDir, sHitLoc, psOffsetTime, damageFromUnderneath, modelIndex, partName, vSurfaceNormal )
|
|
{
|
|
// Don't allow friendlies to kill sarah
|
|
if( !IsPlayer(eAttacker) )
|
|
{
|
|
iDamage = 0;
|
|
return iDamage;
|
|
}
|
|
|
|
iDamage = self killstreaks::OnDamagePerWeapon( "siegebot_theia", eAttacker, iDamage, iDFlags, sMeansOfDeath, weapon, self.maxhealth, undefined, self.maxhealth * 0.4, undefined, 0, undefined, true, 1.0 );
|
|
if( iDamage == 0 )
|
|
return 0;
|
|
|
|
newDamageLevel = vehicle::should_update_damage_fx_level( self.health, iDamage, self.healthdefault );
|
|
if ( newDamageLevel > self.damageLevel )
|
|
{
|
|
self.newDamageLevel = newDamageLevel;
|
|
}
|
|
|
|
if ( self.newDamageLevel > self.damageLevel && pain_canenter() )
|
|
{
|
|
self.damageLevel = self.newDamageLevel;
|
|
self notify( "pain" );
|
|
vehicle::set_damage_fx_level( self.damageLevel );
|
|
|
|
if ( self.damageLevel >= 2 )
|
|
{
|
|
self vehicle::toggle_lights_group( 1, false );
|
|
}
|
|
}
|
|
|
|
return iDamage;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// attack_javelin
|
|
// ----------------------------------------------------------------------------
|
|
function attack_javelin()
|
|
{
|
|
if( level.players.size < 1 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
enemy = array::random( level.players );
|
|
|
|
if ( !isdefined( enemy ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// aim up and fire
|
|
forward = AnglesToForward( self.angles );
|
|
shootpos = self.origin + forward * 200 + (0,0,500);
|
|
//self SetTurretTargetVec( shootpos );
|
|
//self util::waittill_any_timeout( 0.5, "turret_on_target" );
|
|
|
|
self ASMRequestSubstate( "javelin@stationary" );
|
|
self waittill( "fire_javelin" );
|
|
level notify( "theia_preparing_javelin_attack", enemy );
|
|
|
|
current_weapon = self SeatGetWeapon( 0 );
|
|
weapon = GetWeapon( "siegebot_javelin_turret" );
|
|
self thread javeline_incoming(weapon);
|
|
self SetVehWeapon( weapon );
|
|
self thread vehicle_ai::Javelin_LoseTargetAtRightTime( enemy );
|
|
self FireWeapon( 0, enemy );
|
|
|
|
self vehicle_ai::waittill_asm_complete( "javelin@stationary", 3 );
|
|
|
|
self SetVehWeapon( current_weapon );
|
|
|
|
// aim back down
|
|
shootpos = self.origin + forward * 500;
|
|
self SetTurretTargetVec( shootpos );
|
|
self util::waittill_any_timeout( 2, "turret_on_target" );
|
|
|
|
self ClearTurretTarget();
|
|
|
|
if( isdefined( enemy ) && !self VehCanSee( enemy ) ) // use VehCanSee, if recently attacked this will return true and not use FOV check
|
|
{
|
|
forward = AnglesToForward( self.angles );
|
|
|
|
aimpos = self.origin + forward * 1000;
|
|
self SetTurretTargetVec( aimpos );
|
|
msg = self util::waittill_any_timeout( 3.0, "turret_on_target" );
|
|
self ClearTurretTarget();
|
|
}
|
|
|
|
self locomotion_start();
|
|
}
|
|
function javeline_incoming(projectile)
|
|
{
|
|
self endon( "entityshutdown" );
|
|
self endon ("death");
|
|
|
|
self waittill( "weapon_fired", projectile );
|
|
|
|
distance = 1400;
|
|
alias = "prj_javelin_incoming";
|
|
|
|
wait(3);
|
|
|
|
if(!isdefined( projectile ) )
|
|
return;
|
|
|
|
while(isdefined(projectile) && isdefined( projectile.origin ))
|
|
{
|
|
if ( isdefined( self.enemy ) && isdefined( self.enemy.origin ))
|
|
{
|
|
projectileDistance = DistanceSquared( projectile.origin, self.enemy.origin);
|
|
|
|
if( projectileDistance <= distance * distance )
|
|
{
|
|
projectile playsound (alias);
|
|
return;
|
|
}
|
|
}
|
|
|
|
wait (.05);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// attack_spike_minefield
|
|
// ----------------------------------------------------------------------------
|
|
function init_fake_targets()
|
|
{
|
|
count = 6;
|
|
|
|
if( !isdefined( self.spikeFakeTargets ) || self.spikeFakeTargets.size < 1 )
|
|
{
|
|
self.spikeFakeTargets = [];
|
|
for ( i = 0; i < count; i++ )
|
|
{
|
|
newFakeTarget = Spawn( "script_origin", self.origin );
|
|
if ( !isdefined( self.spikeFakeTargets ) ) self.spikeFakeTargets = []; else if ( !IsArray( self.spikeFakeTargets ) ) self.spikeFakeTargets = array( self.spikeFakeTargets ); self.spikeFakeTargets[self.spikeFakeTargets.size]=newFakeTarget;;
|
|
}
|
|
}
|
|
|
|
if( !isdefined( self.fakeTargetEnt ) )
|
|
{
|
|
self.fakeTargetEnt = Spawn( "script_origin", self.origin );
|
|
}
|
|
}
|
|
|
|
// self == spike
|
|
function pin_to_ground()
|
|
{
|
|
trace = BulletTrace( self.origin, self.origin + (0,0,-800), false, self );
|
|
|
|
if( trace["fraction"] < 1.0 )
|
|
{
|
|
self.origin = trace["position"] + (0,0,-20);
|
|
}
|
|
else
|
|
{
|
|
self.origin = self.origin + (0,0,-500);
|
|
}
|
|
}
|
|
|
|
function pin_spike_to_ground()
|
|
{
|
|
self endon("death");
|
|
|
|
wait 0.1;
|
|
|
|
spikeTargets = array::randomize( self.spikeFakeTargets );
|
|
foreach ( target in spikeTargets )
|
|
{
|
|
target pin_to_ground();
|
|
wait randomFloatRange(0.05, 0.1);
|
|
}
|
|
|
|
if ( false )
|
|
{
|
|
foreach ( spike in spikeTargets )
|
|
{
|
|
/#debugstar( spike.origin, 200, (1,0,0) ); #/
|
|
/#Circle( spike.origin, 150, (1,0,0), false, true, 200 ); #/
|
|
}
|
|
}
|
|
}
|
|
|
|
function spike_score( target )
|
|
{
|
|
score = 1.0;
|
|
if ( target IsNoTarget() )
|
|
{
|
|
score = 0.2;
|
|
}
|
|
else if ( !target.allowdeath )
|
|
{
|
|
score = 0.4;
|
|
}
|
|
else if ( IsAirBorne( target ) )
|
|
{
|
|
score = 0.2;
|
|
}
|
|
/*
|
|
else if ( !self VehCanSee( target ) )
|
|
{
|
|
score = 0.6;
|
|
}
|
|
*/
|
|
|
|
return score;
|
|
}
|
|
|
|
function spike_group_score( target, targetList, radius )
|
|
{
|
|
closeTargetScore = spike_score( target );
|
|
foreach ( otherTarget in targetList )
|
|
{
|
|
closeEnough = ( Distance2DSquared( target.origin, otherTarget.origin ) < ( (radius) * (radius) ) );
|
|
if ( closeEnough )
|
|
{
|
|
closeTargetScore = closeTargetScore + spike_score( otherTarget );
|
|
}
|
|
}
|
|
|
|
return closeTargetScore;
|
|
}
|
|
|
|
function attack_spike_minefield()
|
|
{
|
|
spikeCoverRadius = 600;
|
|
randomScale = 40;
|
|
|
|
init_fake_targets();
|
|
|
|
forward = AnglesToForward( self.angles );
|
|
self SetTurretTargetVec( self.origin + forward * 1000 );
|
|
self util::waittill_any_timeout( 2, "turret_on_target" );
|
|
|
|
forward = AnglesToForward( self.angles );
|
|
|
|
targets = GetAITeamArray( "allies" );
|
|
targets = ArrayCombine( targets, level.players, false, false );
|
|
|
|
bestCloseScore = 0.0;
|
|
bestTarget = undefined;
|
|
foreach( target in targets )
|
|
{
|
|
if ( target IsNoTarget() || IsAirBorne( target ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
distanceSelfToTargetSqr = Distance2DSquared( target.origin, self.origin );
|
|
if ( distanceSelfToTargetSqr < ( (500) * (500) ) || distanceSelfToTargetSqr > ( (2100) * (2100) ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
dirToTarget = ( (target.origin - self.origin)[0], (target.origin - self.origin)[1], 0 );
|
|
if ( VectorDot( dirToTarget, forward ) < 0.1 )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
closeTargetScore = spike_group_score( target, targets, spikeCoverRadius );
|
|
|
|
if ( bestCloseScore <= closeTargetScore )
|
|
{
|
|
bestCloseScore = closeTargetScore;
|
|
bestTarget = target;
|
|
}
|
|
}
|
|
|
|
if ( !isdefined( bestTarget ) )
|
|
{
|
|
bestTarget = array::random( GeneratePointsAroundCenter( self.arena_center, 2000, 200 ) );
|
|
}
|
|
else
|
|
{
|
|
bestTarget = bestTarget.origin;
|
|
}
|
|
|
|
if ( false )
|
|
{
|
|
/#debugstar( bestTarget, 200, (1,0,0) ); #/
|
|
/#Circle( bestTarget, spikeCoverRadius, (1,0,0), false, true, 200 ); #/
|
|
}
|
|
|
|
//tell the level theia is about to fire spikes
|
|
level notify( "theia_preparing_spike_attack", bestTarget );
|
|
|
|
targetOrigin = ( bestTarget[0], bestTarget[1], 0 ) + (0,0,self.origin[2]);
|
|
targetPoints = GeneratePointsAroundCenter( targetOrigin, 1200, 120 );
|
|
|
|
numOfSpikeAssigned = 0;
|
|
for( i = 0; i < self.spikeFakeTargets.size && i < targetPoints.size; i++ )
|
|
{
|
|
spike = self.spikeFakeTargets[ i ];
|
|
spike.origin = targetPoints[i];
|
|
numOfSpikeAssigned++;
|
|
}
|
|
|
|
self ASMRequestSubstate( "arm_rocket@stationary" );
|
|
self waittill( "fire_spikes" );
|
|
|
|
for ( i = 0; i < numOfSpikeAssigned; i++ )
|
|
{
|
|
spike = self.spikeFakeTargets[ i ];
|
|
self SetGunnerTargetEnt( spike, (0,0,0), 1 );
|
|
self FireWeapon( 2 );
|
|
wait .05;
|
|
}
|
|
|
|
self thread pin_spike_to_ground();
|
|
|
|
self ClearGunnerTarget( 1 );
|
|
self ClearTurretTarget();
|
|
|
|
self vehicle_ai::waittill_asm_complete( "arm_rocket@stationary", 3 );
|
|
self locomotion_start();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// attack_minigun_sweep
|
|
// ----------------------------------------------------------------------------
|
|
function Delay_Target_ToEnemy_Thread( point, enemy, timeToHit )
|
|
{
|
|
offset = (0, 0, 10);
|
|
|
|
self.fakeTargetEnt Unlink();
|
|
|
|
if ( DistanceSquared( self.fakeTargetEnt.origin, enemy.origin ) > ( (20) * (20) ) )
|
|
{
|
|
self.fakeTargetEnt.origin = point;
|
|
self vehicle_ai::SetTurretTarget( self.fakeTargetEnt, 1 );
|
|
self util::waittill_any_timeout( 2, "turret_on_target" );
|
|
|
|
timeStart = GetTime();
|
|
|
|
while( GetTime() < timeStart + timeToHit * 1000 )
|
|
{
|
|
self.fakeTargetEnt.origin = LerpVector( point, enemy.origin + offset, ( GetTime() - timeStart ) / ( timeToHit * 1000 ) );
|
|
if ( false ) /#debugstar( self.fakeTargetEnt.origin, 100, (0,1,0) ); #/
|
|
{wait(.05);};
|
|
}
|
|
}
|
|
|
|
self.fakeTargetEnt.origin = enemy.origin + offset;
|
|
{wait(.05);};
|
|
self.fakeTargetEnt LinkTo( enemy );
|
|
}
|
|
|
|
function is_valid_target( target )
|
|
{
|
|
if ( ( isdefined( target.ignoreme ) && target.ignoreme ) || ( target.health <= 0 ) )
|
|
{
|
|
return false;
|
|
}
|
|
else if ( isPlayer( target ) && target laststand::player_is_in_laststand() )
|
|
{
|
|
return false;
|
|
}
|
|
else if ( IsSentient( target ) && ( target IsNoTarget() || !IsAlive( target ) ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function get_enemy()
|
|
{
|
|
if ( isdefined( self.enemy ) && is_valid_target( self.enemy ) )
|
|
{
|
|
return self.enemy;
|
|
}
|
|
|
|
targets = GetAITeamArray( "allies" );
|
|
targets = ArrayCombine( targets, level.players, false, false );
|
|
|
|
validTargets = [];
|
|
foreach( target in targets )
|
|
{
|
|
if ( is_valid_target( target ) )
|
|
{
|
|
if ( !isdefined( validTargets ) ) validTargets = []; else if ( !IsArray( validTargets ) ) validTargets = array( validTargets ); validTargets[validTargets.size]=target;;
|
|
}
|
|
}
|
|
|
|
targets = array::merge_sort( validTargets, &_sort_by_distance2d, self.origin );
|
|
return targets[0];
|
|
}
|
|
|
|
function attack_minigun_sweep()
|
|
{
|
|
duration = 4;
|
|
interval = 1;
|
|
self.turretrotscale = 0.4; // how fast to rotate the upper body turret
|
|
|
|
self ClearTurretTarget();
|
|
self ClearGunnerTarget( 1 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 0 );
|
|
self SetTurretTargetRelativeAngles( (0,0,0), 1 );
|
|
self ASMRequestSubstate( "sweep@gun" );
|
|
self waittill( "barrelspin_start" );
|
|
self clientfield::set( "sarah_minigun_spin", 1 );
|
|
self SetTurretSpinning( true );
|
|
|
|
self waittill( "barrelspin_loop" );
|
|
|
|
enemy = get_enemy();
|
|
vectorFromEnemy = VectorNormalize( ( (self.origin - enemy.origin)[0], (self.origin - enemy.origin)[1], 0 ) );
|
|
position = enemy.origin + vectorFromEnemy * 500;
|
|
stopTime = GetTime() + duration * 1000;
|
|
self thread vehicle_ai::fire_for_time( duration * 2, 1 );
|
|
|
|
while ( GetTime() < stopTime )
|
|
{
|
|
enemy = get_enemy();
|
|
|
|
v_gunner_barrel1 = self GetTagOrigin( "tag_gunner_flash1" );
|
|
v_bullet_trace_end = enemy.origin + ( 0, 0, 30 );
|
|
trace = BulletTrace( v_gunner_barrel1, v_bullet_trace_end, true, enemy );
|
|
if( trace["fraction"] == 1 )
|
|
{
|
|
self GetPerfectInfo( enemy, true );
|
|
}
|
|
else if ( !IsPlayer(enemy) )
|
|
{
|
|
self SetPersonalThreatBias( enemy, -2000, 3.0 );
|
|
}
|
|
|
|
if( !enemy.allowdeath && !IsPlayer(enemy) )
|
|
{
|
|
self SetPersonalThreatBias( enemy, -900, 8.0 );
|
|
}
|
|
|
|
self vehicle_ai::SetTurretTarget( enemy, 0 );
|
|
|
|
if ( IsPlayer( enemy ) )
|
|
{
|
|
vectorFromEnemy = VectorNormalize( ( (self.origin - enemy.origin)[0], (self.origin - enemy.origin)[1], 0 ) );
|
|
self Delay_Target_ToEnemy_Thread( enemy.origin + vectorFromEnemy * 500, enemy, 0.7 );
|
|
}
|
|
else
|
|
{
|
|
self vehicle_ai::SetTurretTarget( enemy, 1 );
|
|
}
|
|
|
|
self util::waittill_any_timeout( interval, "enemy" );
|
|
}
|
|
|
|
self SetTurretSpinning( false );
|
|
self notify( "fire_stop" );
|
|
|
|
self locomotion_start();
|
|
|
|
self waittill( "barrelspin_end" );
|
|
self clientfield::set( "sarah_minigun_spin", 0 );
|
|
|
|
self.turretrotscale = 1.0;
|
|
|
|
wait 0.2;
|
|
}
|
|
|