1726 lines
51 KiB
Plaintext
1726 lines
51 KiB
Plaintext
#using scripts\codescripts\struct;
|
|
|
|
#using scripts\shared\gameskill_shared;
|
|
#using scripts\shared\hostmigration_shared;
|
|
#using scripts\shared\math_shared;
|
|
#using scripts\shared\array_shared;
|
|
#using scripts\shared\statemachine_shared;
|
|
#using scripts\shared\system_shared;
|
|
#using scripts\shared\util_shared;
|
|
#using scripts\shared\vehicle_shared;
|
|
#using scripts\shared\vehicle_death_shared;
|
|
#using scripts\shared\vehicle_ai_shared;
|
|
#using scripts\shared\vehicles\_attack_drone;
|
|
#using scripts\shared\flag_shared;
|
|
#using scripts\shared\turret_shared;
|
|
#using scripts\shared\ai_shared;
|
|
#using scripts\shared\ai\systems\ai_interface;
|
|
//#using scripts\cp\_util; // for getOtherTeam function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Speeds
|
|
|
|
|
|
|
|
|
|
// scanner
|
|
|
|
|
|
// 0 as horizontal, 90 as straight down
|
|
|
|
|
|
//90
|
|
|
|
|
|
// weapon
|
|
|
|
|
|
|
|
// tunable length of persistent sight on target before fire rocket
|
|
|
|
|
|
// drone
|
|
|
|
// will wait this amount of time after player run into a non-reachable space to deploy attack drones
|
|
|
|
// other
|
|
|
|
|
|
|
|
|
|
|
|
// pain
|
|
// 0 to disable
|
|
|
|
// feature control
|
|
|
|
// if true, side turret will not choose same target as main turret (so the player being targeted won't get cross fired by all three turrets)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#namespace hunter;
|
|
|
|
function autoexec __init__sytem__() { system::register("hunter",&__init__,undefined,undefined); }
|
|
|
|
#using_animtree( "generic" );
|
|
|
|
// ----------------------------------------------
|
|
// initialization
|
|
// ----------------------------------------------
|
|
function __init__()
|
|
{
|
|
RegisterInterfaceAttributes( "hunter" );
|
|
vehicle::add_main_callback( "hunter", &hunter_initialize );
|
|
}
|
|
|
|
function RegisterInterfaceAttributes( archetype )
|
|
{
|
|
vehicle_ai::RegisterSharedInterfaceAttributes( archetype );
|
|
|
|
// Strafe distance
|
|
ai::RegisterNumericInterface(
|
|
archetype,
|
|
"strafe_speed",
|
|
0,
|
|
0, 100 );
|
|
|
|
ai::RegisterNumericInterface(
|
|
archetype,
|
|
"strafe_distance",
|
|
0,
|
|
0, 10000 );
|
|
}
|
|
|
|
function hunter_initTagArrays()
|
|
{
|
|
self.weakSpotTags = [];
|
|
if ( false )
|
|
{
|
|
if ( !isdefined( self.weakSpotTags ) ) self.weakSpotTags = []; else if ( !IsArray( self.weakSpotTags ) ) self.weakSpotTags = array( self.weakSpotTags ); self.weakSpotTags[self.weakSpotTags.size]="tag_target_l";;
|
|
if ( !isdefined( self.weakSpotTags ) ) self.weakSpotTags = []; else if ( !IsArray( self.weakSpotTags ) ) self.weakSpotTags = array( self.weakSpotTags ); self.weakSpotTags[self.weakSpotTags.size]="tag_target_r";;
|
|
}
|
|
|
|
self.explosiveWeakSpotTags = [];
|
|
if ( false )
|
|
{
|
|
if ( !isdefined( self.explosiveWeakSpotTags ) ) self.explosiveWeakSpotTags = []; else if ( !IsArray( self.explosiveWeakSpotTags ) ) self.explosiveWeakSpotTags = array( self.explosiveWeakSpotTags ); self.explosiveWeakSpotTags[self.explosiveWeakSpotTags.size]="tag_fan_base_l";;
|
|
if ( !isdefined( self.explosiveWeakSpotTags ) ) self.explosiveWeakSpotTags = []; else if ( !IsArray( self.explosiveWeakSpotTags ) ) self.explosiveWeakSpotTags = array( self.explosiveWeakSpotTags ); self.explosiveWeakSpotTags[self.explosiveWeakSpotTags.size]="tag_fan_base_r";;
|
|
}
|
|
|
|
self.missileTags = [];
|
|
if ( !isdefined( self.missileTags ) ) self.missileTags = []; else if ( !IsArray( self.missileTags ) ) self.missileTags = array( self.missileTags ); self.missileTags[self.missileTags.size]="tag_rocket1";;
|
|
if ( !isdefined( self.missileTags ) ) self.missileTags = []; else if ( !IsArray( self.missileTags ) ) self.missileTags = array( self.missileTags ); self.missileTags[self.missileTags.size]="tag_rocket2";;
|
|
|
|
self.droneAttachTags = [];
|
|
if ( false )
|
|
{
|
|
if ( !isdefined( self.droneAttachTags ) ) self.droneAttachTags = []; else if ( !IsArray( self.droneAttachTags ) ) self.droneAttachTags = array( self.droneAttachTags ); self.droneAttachTags[self.droneAttachTags.size]="tag_drone_attach_l";;
|
|
if ( !isdefined( self.droneAttachTags ) ) self.droneAttachTags = []; else if ( !IsArray( self.droneAttachTags ) ) self.droneAttachTags = array( self.droneAttachTags ); self.droneAttachTags[self.droneAttachTags.size]="tag_drone_attach_r";;
|
|
}
|
|
}
|
|
|
|
function hunter_SpawnDrones()
|
|
{
|
|
self.dronesOwned = [];
|
|
|
|
if ( false )
|
|
{
|
|
foreach( droneTag in self.droneAttachTags )
|
|
{
|
|
origin = self GetTagOrigin( droneTag );
|
|
angles = self GetTagAngles( droneTag );
|
|
|
|
drone = SpawnVehicle( "spawner_bo3_attack_drone_enemy", origin, angles );
|
|
|
|
drone.owner = self;
|
|
drone.attachTag = droneTag;
|
|
drone.team = self.team;
|
|
|
|
if ( !isdefined( self.dronesOwned ) ) self.dronesOwned = []; else if ( !IsArray( self.dronesOwned ) ) self.dronesOwned = array( self.dronesOwned ); self.dronesOwned[self.dronesOwned.size]=drone;;
|
|
}
|
|
}
|
|
}
|
|
|
|
function hunter_initialize()
|
|
{
|
|
self endon( "death" );
|
|
|
|
self UseAnimTree( #animtree );
|
|
|
|
Target_Set( self, ( 0, 0, 90 ) );
|
|
|
|
ai::CreateInterfaceForEntity( self );
|
|
|
|
self.health = self.healthdefault;
|
|
|
|
self vehicle::friendly_fire_shield();
|
|
|
|
//self EnableAimAssist();
|
|
self SetNearGoalNotifyDist( 50 );
|
|
|
|
self SetHoverParams( 15, 100, 40 );
|
|
self.flyheight = GetDvarFloat( "g_quadrotorFlyHeight" );
|
|
|
|
self.fovcosine = 0; // +/-90 degrees = 180
|
|
self.fovcosinebusy = 0.574; //+/- 55 degrees = 110 fov
|
|
|
|
self.vehAirCraftCollisionEnabled = true;
|
|
|
|
self.original_vehicle_type = self.vehicletype;
|
|
|
|
self.settings = struct::get_script_bundle( "vehiclecustomsettings", self.scriptbundlesettings );
|
|
|
|
self.goalRadius = 999999;
|
|
self.goalHeight = 999999;
|
|
self SetGoal( self.origin, false, self.goalRadius, self.goalHeight );
|
|
|
|
self hunter_initTagArrays();
|
|
|
|
self.overrideVehicleDamage = &HunterCallback_VehicleDamage;
|
|
|
|
self thread vehicle_ai::nudge_collision();
|
|
|
|
//disable some cybercom abilities
|
|
if( IsDefined( level.vehicle_initializer_cb ) )
|
|
{
|
|
[[level.vehicle_initializer_cb]]( self );
|
|
}
|
|
|
|
self.ignoreFireFly = true;
|
|
self.ignoreDecoy = true;
|
|
self vehicle_ai::InitThreatBias();
|
|
|
|
// self thread hunter_frontScanning();
|
|
|
|
// self hunter_SpawnDrones();
|
|
//
|
|
// wait 0.5;
|
|
//
|
|
// foreach ( drone in self.dronesOwned )
|
|
// {
|
|
// if ( isalive( drone ) )
|
|
// {
|
|
// drone vehicle_ai::set_state( "attached" );
|
|
// }
|
|
// }
|
|
self turret::_init_turret( 1 );
|
|
self turret::_init_turret( 2 );
|
|
|
|
self turret::set_best_target_func( &side_turret_get_best_target, 1 );
|
|
self turret::set_best_target_func( &side_turret_get_best_target, 2 );
|
|
|
|
self turret::set_burst_parameters( 1, 2, 1, 2, 1 );
|
|
self turret::set_burst_parameters( 1, 2, 1, 2, 2 );
|
|
|
|
self turret::set_target_flags( 1 | 2, 1 );
|
|
self turret::set_target_flags( 1 | 2, 2 );
|
|
|
|
self side_turrets_forward();
|
|
|
|
self PathVariableOffset( (10, 10, -30), 1 );
|
|
|
|
defaultRole();
|
|
}
|
|
|
|
function defaultRole()
|
|
{
|
|
self vehicle_ai::init_state_machine_for_role();
|
|
|
|
self vehicle_ai::get_state_callbacks( "combat" ).enter_func = &state_combat_enter;
|
|
self vehicle_ai::get_state_callbacks( "combat" ).update_func = &state_combat_update;
|
|
self vehicle_ai::get_state_callbacks( "combat" ).exit_func = &state_combat_exit;
|
|
|
|
self vehicle_ai::get_state_callbacks( "driving" ).enter_func = &hunter_scripted;
|
|
self vehicle_ai::get_state_callbacks( "scripted" ).enter_func = &hunter_scripted;
|
|
|
|
self vehicle_ai::get_state_callbacks( "death" ).enter_func = &state_death_enter;
|
|
self vehicle_ai::get_state_callbacks( "death" ).update_func = &state_death_update;
|
|
|
|
self vehicle_ai::get_state_callbacks( "emped" ).update_func = &hunter_emped;
|
|
|
|
self vehicle_ai::add_state( "unaware",
|
|
undefined,
|
|
&state_unaware_update,
|
|
&state_unaware_exit );
|
|
|
|
vehicle_ai::add_interrupt_connection( "unaware", "scripted", "enter_scripted" );
|
|
vehicle_ai::add_interrupt_connection( "unaware", "emped", "emped" );
|
|
vehicle_ai::add_interrupt_connection( "unaware", "off", "shut_off" );
|
|
vehicle_ai::add_interrupt_connection( "unaware", "driving", "enter_vehicle" );
|
|
vehicle_ai::add_interrupt_connection( "unaware", "pain", "pain" );
|
|
|
|
self vehicle_ai::add_state( "strafe",
|
|
&state_strafe_enter,
|
|
&state_strafe_update,
|
|
&state_strafe_exit );
|
|
|
|
vehicle_ai::add_interrupt_connection( "strafe", "scripted", "enter_scripted" );
|
|
vehicle_ai::add_interrupt_connection( "strafe", "emped", "emped" );
|
|
vehicle_ai::add_interrupt_connection( "strafe", "off", "shut_off" );
|
|
vehicle_ai::add_interrupt_connection( "strafe", "driving", "enter_vehicle" );
|
|
vehicle_ai::add_interrupt_connection( "strafe", "pain", "pain" );
|
|
vehicle_ai::add_utility_connection( "strafe", "combat" );
|
|
|
|
vehicle_ai::add_utility_connection( "emped", "strafe" );
|
|
vehicle_ai::add_utility_connection( "pain", "strafe" );
|
|
|
|
vehicle_ai::StartInitialState();
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: death
|
|
// ----------------------------------------------
|
|
function shut_off_fx()
|
|
{
|
|
self endon( "death" );
|
|
|
|
self notify( "death_shut_off" );
|
|
|
|
if ( isdefined( self.frontScanner ) )
|
|
{
|
|
self.frontScanner.sndScanningEnt delete();
|
|
self.frontScanner delete();
|
|
}
|
|
}
|
|
|
|
function kill_drones()
|
|
{
|
|
self endon( "death" );
|
|
|
|
foreach ( drone in self.dronesOwned )
|
|
{
|
|
if ( isalive( drone ) && Distance2DSquared( self.origin, drone.origin ) < ( (80) * (80) ) )
|
|
{
|
|
damageOrigin = self.origin + (0,0,1);
|
|
drone finishVehicleRadiusDamage(self.death_info.attacker, self.death_info.attacker, 32000, 32000, 10, 0, "MOD_EXPLOSIVE", level.weaponNone, damageOrigin, 400, -1, (0,0,1), 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
function state_death_enter( params )
|
|
{
|
|
self endon( "death" );
|
|
|
|
if ( isdefined( self.fakeTargetEnt ) )
|
|
{
|
|
self.fakeTargetEnt Delete();
|
|
}
|
|
|
|
vehicle_ai::defaultstate_death_enter();
|
|
|
|
self.inpain = true;
|
|
|
|
self thread shut_off_fx();
|
|
//self thread kill_drones();
|
|
}
|
|
|
|
function state_death_update( params )
|
|
{
|
|
self endon( "death" );
|
|
|
|
death_type = vehicle_ai::get_death_type( params );
|
|
if ( !isdefined( death_type ) )
|
|
{
|
|
params.death_type = "gibbed";
|
|
death_type = params.death_type;
|
|
}
|
|
|
|
self vehicle_ai::ClearAllLookingAndTargeting();
|
|
self vehicle_ai::ClearAllMovement();
|
|
self CancelAIMove();
|
|
self SetSpeedImmediate( 0 );
|
|
self SetVehVelocity( ( 0, 0, 0 ) );
|
|
self SetPhysAcceleration( ( 0, 0, 0 ) );
|
|
self SetAngularVelocity( ( 0, 0, 0 ) );
|
|
self vehicle_ai::defaultstate_death_update( params );
|
|
}
|
|
// ----------------------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: unaware
|
|
// ----------------------------------------------
|
|
function state_unaware_enter( params )
|
|
{
|
|
ratio = 0.5;
|
|
accel = self GetDefaultAcceleration();
|
|
self SetSpeed( ratio * self.settings.defaultmovespeed, ratio * accel, ratio * accel );
|
|
}
|
|
|
|
function state_unaware_update( params )
|
|
{
|
|
self endon( "change_state" );
|
|
self endon( "death" );
|
|
|
|
if ( isdefined( self.enemy ) )
|
|
{
|
|
self vehicle_ai::set_state( "combat" );
|
|
}
|
|
|
|
self ClearLookAtEnt();
|
|
self disable_turrets();
|
|
self thread Movement_Thread_Wander();
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "enemy" );
|
|
self vehicle_ai::set_state( "combat" );
|
|
}
|
|
}
|
|
|
|
function state_unaware_exit( params )
|
|
{
|
|
self notify( "end_movement_thread" );
|
|
}
|
|
|
|
function Movement_Thread_Wander()
|
|
{
|
|
self endon( "death" );
|
|
self notify( "end_movement_thread" );
|
|
self endon( "end_movement_thread" );
|
|
|
|
constMinSearchRadius = 120;
|
|
constMaxSearchRadius = 800;
|
|
|
|
minSearchRadius = math::clamp( constMinSearchRadius, 0, self.goalRadius );
|
|
maxSearchRadius = math::clamp( constMaxSearchRadius, constMinSearchRadius, self.goalRadius );
|
|
halfHeight = 400;
|
|
innerSpacing = 80;
|
|
outerSpacing = 50;
|
|
maxGoalTimeout = 15;
|
|
timeAtSamePosition = 2.5 + randomfloat( 1 );
|
|
|
|
while ( true )
|
|
{
|
|
queryResult = PositionQuery_Source_Navigation( self.origin, minSearchRadius, maxSearchRadius, halfHeight, innerSpacing, self, outerSpacing );
|
|
PositionQuery_Filter_DistanceToGoal( queryResult, self );
|
|
vehicle_ai::PositionQuery_Filter_OutOfGoalAnchor( queryResult );
|
|
vehicle_ai::PositionQuery_Filter_Random( queryResult, 0, 10 );
|
|
vehicle_ai::PositionQuery_PostProcess_SortScore( queryResult );
|
|
|
|
stayAtGoal = timeAtSamePosition > 0.2;
|
|
|
|
foundpath = false;
|
|
for ( i = 0; i < queryResult.data.size && !foundpath; i++ )
|
|
{
|
|
goalPos = queryResult.data[i].origin;
|
|
foundpath = self SetVehGoalPos( goalPos, stayAtGoal, true );
|
|
}
|
|
|
|
if ( foundPath )
|
|
{
|
|
msg = self util::waittill_any_timeout( maxGoalTimeout, "near_goal", "force_goal", "reached_end_node", "goal" );
|
|
|
|
if ( stayAtGoal )
|
|
{
|
|
wait randomFloatRange( 0.5 * timeAtSamePosition, timeAtSamePosition );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
wait 1;
|
|
}
|
|
}
|
|
}
|
|
// ----------------------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: combat
|
|
// ----------------------------------------------
|
|
function enable_turrets()
|
|
{
|
|
//self turret::enable( 0, false );
|
|
self turret::enable( 1, false );
|
|
self turret::enable( 2, false );
|
|
}
|
|
|
|
function disable_turrets()
|
|
{
|
|
//self turret::disable( 0 );
|
|
self turret::disable( 1 );
|
|
self turret::disable( 2 );
|
|
self side_turrets_forward();
|
|
}
|
|
|
|
function side_turrets_forward()
|
|
{
|
|
self SetTurretTargetRelativeAngles( (10, -90, 0), 1 );
|
|
self SetTurretTargetRelativeAngles( (10, 90, 0), 2 );
|
|
}
|
|
|
|
function state_combat_enter( params )
|
|
{
|
|
ratio = 1.0;
|
|
accel = self GetDefaultAcceleration();
|
|
self SetSpeed( ratio * self.settings.defaultmovespeed, ratio * accel, ratio * accel );
|
|
|
|
self hunter_lockon_fx();
|
|
|
|
self enable_turrets();
|
|
}
|
|
|
|
function state_combat_update( params )
|
|
{
|
|
self endon( "change_state" );
|
|
self endon( "death" );
|
|
|
|
if ( !isdefined( self.enemy ) )
|
|
{
|
|
self vehicle_ai::set_state( "unaware" );
|
|
}
|
|
|
|
self thread Movement_Thread_StayInDistance();
|
|
self thread Attack_Thread_MainTurret();
|
|
self thread Attack_Thread_rocket();
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "no_enemy" );
|
|
self vehicle_ai::set_state( "unaware" );
|
|
}
|
|
}
|
|
|
|
function state_combat_exit( params )
|
|
{
|
|
self notify( "end_attack_thread" );
|
|
self notify( "end_movement_thread" );
|
|
self ClearTurretTarget();
|
|
}
|
|
// ----------------------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: strafe
|
|
// ----------------------------------------------
|
|
function state_strafe_enter( params )
|
|
{
|
|
ratio = 2.0;
|
|
accel = ratio * self GetDefaultAcceleration();
|
|
speed = ratio * self.settings.defaultmovespeed;
|
|
strafe_speed_attribute = ai::get_behavior_attribute("strafe_speed");
|
|
if ( strafe_speed_attribute > 0 )
|
|
{
|
|
speed = strafe_speed_attribute;
|
|
}
|
|
self SetSpeed( speed , accel, accel );
|
|
}
|
|
|
|
function state_strafe_update( params )
|
|
{
|
|
self endon( "change_state" );
|
|
self endon( "death" );
|
|
|
|
self ClearVehGoalPos();
|
|
|
|
distanceToTarget = 0.5 * ( self.settings.engagementDistMin + self.settings.engagementDistMax );
|
|
|
|
target = self.origin + AnglesToForward( self.angles ) * distanceToTarget;
|
|
if ( isdefined( self.enemy ) )
|
|
{
|
|
distanceToTarget = Distance( self.origin, self.enemy.origin );
|
|
}
|
|
|
|
distanceThreshold = 500 + distanceToTarget * 0.08;
|
|
strafe_distance_attribute = ai::get_behavior_attribute("strafe_distance");
|
|
if ( strafe_distance_attribute > 0 )
|
|
{
|
|
distanceThreshold = strafe_distance_attribute;
|
|
}
|
|
|
|
maxSearchRadius = distanceThreshold * 1.5;
|
|
halfHeight = 300;
|
|
outerSpacing = maxSearchRadius * 0.05;
|
|
innerSpacing = outerSpacing * 2;
|
|
|
|
queryResult = PositionQuery_Source_Navigation( self.origin, 0, maxSearchRadius, halfHeight, innerSpacing, self, outerSpacing );
|
|
PositionQuery_Filter_Directness( queryResult, self.origin, target );
|
|
PositionQuery_Filter_DistanceToGoal( queryResult, self );
|
|
PositionQuery_Filter_InClaimedLocation( queryResult, self );
|
|
self vehicle_ai::PositionQuery_Filter_OutOfGoalAnchor( queryResult, 200 );
|
|
|
|
foreach ( point in queryResult.data )
|
|
{
|
|
distanceToPointSqr = distanceSquared( point.origin, self.origin );
|
|
if( distanceToPointSqr < distanceThreshold * 0.5 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "distAway" ] = -distanceThreshold; #/ point.score += -distanceThreshold;;
|
|
}
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "distAway" ] = sqrt( distanceToPointSqr ); #/ point.score += sqrt( distanceToPointSqr );;
|
|
|
|
diffToPreferedDirectness = abs( point.directness - 0 );
|
|
directnessScore = MapFloat( 0, 1, 1000, 0, diffToPreferedDirectness );
|
|
if ( diffToPreferedDirectness > 0.1 )
|
|
{
|
|
directnessScore -= 500;
|
|
}
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "directnessRaw" ] = point.directness; #/ point.score += point.directness;;
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "directness" ] = directnessScore; #/ point.score += directnessScore;;
|
|
|
|
if ( point.directionChange < 0.6 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "directionChange" ] = -2000; #/ point.score += -2000;;
|
|
}
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "directionChangeRaw" ] = point.directionChange; #/ point.score += point.directionChange;;
|
|
}
|
|
|
|
vehicle_ai::PositionQuery_PostProcess_SortScore( queryResult );
|
|
self vehicle_ai::PositionQuery_DebugScores( queryResult );
|
|
|
|
foreach ( point in queryResult.data )
|
|
{
|
|
self.current_pathto_pos = point.origin;
|
|
|
|
foundpath = self SetVehGoalPos( self.current_pathto_pos, true, true );
|
|
|
|
if ( foundPath )
|
|
{
|
|
msg = self util::waittill_any_timeout( 5, "near_goal", "force_goal", "goal", "enemy_visible" );
|
|
break;
|
|
}
|
|
}
|
|
|
|
previous_state = self vehicle_ai::get_previous_state();
|
|
|
|
if ( !isdefined( previous_state ) || previous_state == "strafe" )
|
|
{
|
|
previous_state = "combat";
|
|
}
|
|
|
|
self vehicle_ai::set_state( previous_state );
|
|
}
|
|
|
|
function state_strafe_exit( params )
|
|
{
|
|
vehicle_ai::Cooldown( "strafe_again", 2.0 );
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
|
|
function GetNextMovePosition_tactical( enemy )
|
|
{
|
|
if( self.goalforced )
|
|
{
|
|
return self.goalpos;
|
|
}
|
|
|
|
selfDistToEnemy = Distance2D( self.origin, enemy.origin );
|
|
|
|
// distance based multipliers
|
|
goodDist = 0.5 * ( self.settings.engagementDistMin + self.settings.engagementDistMax );
|
|
|
|
tooCloseDist = 0.8 * goodDist;
|
|
closeDist = 1.2 * goodDist;
|
|
farDist = 3 * goodDist;
|
|
|
|
queryMultiplier = MapFloat( closeDist, farDist, 1, 3, selfDistToEnemy );
|
|
|
|
preferedDistAwayFromOrigin = 150;
|
|
|
|
maxSearchRadius = 1000 * queryMultiplier;
|
|
halfHeight = 300 * queryMultiplier;
|
|
innerSpacing = 80 * queryMultiplier;
|
|
outerSpacing = 80 * queryMultiplier;
|
|
|
|
queryResult = PositionQuery_Source_Navigation( self.origin, 0, maxSearchRadius, halfHeight, innerSpacing, self, outerSpacing );
|
|
PositionQuery_Filter_DistanceToGoal( queryResult, self );
|
|
PositionQuery_Filter_InClaimedLocation( queryResult, self );
|
|
PositionQuery_Filter_Sight( queryResult, enemy.origin, self GetEye() - self.origin, self, 0, enemy );
|
|
self vehicle_ai::PositionQuery_Filter_OutOfGoalAnchor( queryResult, 200 );
|
|
self vehicle_ai::PositionQuery_Filter_EngagementDist( queryResult, enemy, self.settings.engagementDistMin, self.settings.engagementDistMax );
|
|
self vehicle_ai::PositionQuery_Filter_Random( queryResult, 0, 30 );
|
|
|
|
goalHeight = enemy.origin[2] + 0.5 * ( self.settings.engagementHeightMin + self.settings.engagementHeightMax );
|
|
|
|
foreach ( point in queryResult.data )
|
|
{
|
|
if ( !point.visibility )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "no visibility" ] = -600; #/ point.score += -600;;
|
|
}
|
|
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "engagementDist" ] = -point.distAwayFromEngagementArea; #/ point.score += -point.distAwayFromEngagementArea;;
|
|
|
|
// distance from origin
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "distToOrigin" ] = MapFloat( 0, preferedDistAwayFromOrigin, 0, 600, point.distToOrigin2D ); #/ point.score += MapFloat( 0, preferedDistAwayFromOrigin, 0, 600, point.distToOrigin2D );;
|
|
|
|
if( point.inClaimedLocation )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "inClaimedLocation" ] = -500; #/ point.score += -500;;
|
|
}
|
|
|
|
// height
|
|
preferedHeightRange = 75;
|
|
distFromPreferredHeight = abs( point.origin[2] - goalHeight );
|
|
if ( distFromPreferredHeight > preferedHeightRange )
|
|
{
|
|
heightScore = -MapFloat( preferedHeightRange, 5000, 0, 9000, distFromPreferredHeight );
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "height" ] = heightScore; #/ point.score += heightScore;;
|
|
}
|
|
}
|
|
self vehicle_ai::PositionQuery_DebugScores( queryResult );
|
|
|
|
vehicle_ai::PositionQuery_PostProcess_SortScore( queryResult );
|
|
|
|
if( queryResult.data.size )
|
|
{
|
|
return queryResult.data[0].origin;
|
|
}
|
|
|
|
return self.origin;
|
|
}
|
|
|
|
function Movement_Thread_StayInDistance()
|
|
{
|
|
self endon( "death" );
|
|
self notify( "end_movement_thread" );
|
|
self endon( "end_movement_thread" );
|
|
|
|
maxGoalTimeout = 10;
|
|
|
|
stuckCount = 0;
|
|
|
|
while ( true )
|
|
{
|
|
enemy = self.enemy;
|
|
if ( !isdefined( enemy ) )
|
|
{
|
|
wait 1;
|
|
continue;
|
|
}
|
|
|
|
usePathfinding = true;
|
|
onNavVolume = IsPointInNavVolume( self.origin, "navvolume_big" );
|
|
if ( !onNavVolume )
|
|
{
|
|
// off nav volume, try getting back
|
|
getbackPoint = undefined;
|
|
pointOnNavVolume = self GetClosestPointOnNavVolume( self.origin, 500 );
|
|
if ( isdefined( pointOnNavVolume ) )
|
|
{
|
|
if ( SightTracePassed( self.origin, pointOnNavVolume, false, self ) )
|
|
{
|
|
getbackPoint = pointOnNavVolume;
|
|
}
|
|
}
|
|
|
|
if ( !isdefined( getbackPoint ) )
|
|
{
|
|
queryResult = PositionQuery_Source_Navigation( self.origin, 0, 800, 400, 1.5 * self.radius );
|
|
PositionQuery_Filter_Sight( queryResult, self.origin, (0, 0, 0), self, 1 );
|
|
getbackPoint = undefined;
|
|
foreach( point in queryResult.data )
|
|
{
|
|
if ( point.visibility === true )
|
|
{
|
|
getbackPoint = point.origin;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( isdefined( getbackPoint ) )
|
|
{
|
|
self.current_pathto_pos = getbackPoint;
|
|
usePathfinding = false;
|
|
}
|
|
else
|
|
{
|
|
stuckCount++;
|
|
if ( stuckCount == 1 )
|
|
{
|
|
stuckLocation = self.origin;
|
|
}
|
|
else if ( stuckCount > 10 )
|
|
{
|
|
/#
|
|
assert( false, "Hunter fall outside of NavVolume at " + self.origin );
|
|
v_box_min = ( -self.radius, -self.radius, -self.radius );
|
|
v_box_max = ( self.radius, self.radius, self.radius );
|
|
Box( self.origin, v_box_min, v_box_max, self.angles[1], (1,0,0), 1, false, 1000000 );
|
|
if ( isdefined( stuckLocation ) )
|
|
{
|
|
Line( stuckLocation, self.origin, (1,0,0), 1, true, 1000000 );
|
|
}
|
|
#/
|
|
self Kill();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
stuckCount = 0;
|
|
|
|
if( self.goalforced )
|
|
{
|
|
goalpos = self GetClosestPointOnNavVolume( self.goalpos, 200 );
|
|
if ( isdefined( goalpos ) )
|
|
{
|
|
self.current_pathto_pos = goalpos;
|
|
usePathfinding = true;
|
|
}
|
|
else
|
|
{
|
|
self.current_pathto_pos = self.goalpos;
|
|
usePathfinding = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self.current_pathto_pos = GetNextMovePosition_tactical( enemy );
|
|
usePathfinding = true;
|
|
}
|
|
}
|
|
|
|
if ( !isDefined( self.current_pathto_pos ) )
|
|
{
|
|
wait 0.5;
|
|
continue;
|
|
}
|
|
|
|
distanceToGoalSq = DistanceSquared( self.current_pathto_pos, self.origin );
|
|
|
|
if ( distanceToGoalSq > ( (0.5 * ( self.settings.engagementDistMin + self.settings.engagementDistMax )) * (0.5 * ( self.settings.engagementDistMin + self.settings.engagementDistMax )) ) )
|
|
{
|
|
self SetSpeed( self.settings.defaultMoveSpeed * 2 );
|
|
}
|
|
else
|
|
{
|
|
self SetSpeed( self.settings.defaultMoveSpeed );
|
|
}
|
|
|
|
self SetLookAtEnt( enemy );
|
|
|
|
foundpath = self SetVehGoalPos( self.current_pathto_pos, true, usePathfinding );
|
|
|
|
if ( foundPath )
|
|
{
|
|
/#
|
|
if ( ( isdefined( GetDvarInt("hkai_debugPositionQuery") ) && GetDvarInt("hkai_debugPositionQuery") ) )
|
|
{
|
|
recordLine( self.origin, self.current_pathto_pos, (0.3,1,0) );
|
|
recordLine( self.origin, enemy.origin, (1,0,0.4) );
|
|
}
|
|
#/
|
|
|
|
msg = self util::waittill_any_timeout( maxGoalTimeout, "near_goal", "force_goal", "goal" );
|
|
}
|
|
else
|
|
{
|
|
wait 0.5;
|
|
}
|
|
|
|
enemy = self.enemy;
|
|
if ( isdefined( enemy ) )
|
|
{
|
|
goalHeight = enemy.origin[2] + 0.5 * ( self.settings.engagementHeightMin + self.settings.engagementHeightMax );
|
|
distFromPreferredHeight = abs( self.origin[2] - goalHeight );
|
|
|
|
farDist = self.settings.engagementDistMax;
|
|
nearDist = self.settings.engagementDistMin;
|
|
|
|
selfDistToEnemy = Distance2D( self.origin, enemy.origin );
|
|
|
|
if ( self VehCanSee( enemy ) && selfDistToEnemy < farDist && selfDistToEnemy > nearDist && distFromPreferredHeight < 230 )
|
|
{
|
|
msg = self util::waittill_any_timeout( RandomFloatRange( 2, 4 ), "enemy_not_visible" );
|
|
if ( msg == "enemy_not_visible" )
|
|
{
|
|
msg = self util::waittill_any_timeout( 1.0, "enemy_visible" );
|
|
if ( msg != "timeout" )
|
|
{
|
|
wait 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
wait 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
function Delay_Target_ToEnemy_Thread( point, enemy, timeToHit )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self endon( "end_attack_thread" );
|
|
self endon( "faketarget_stop_moving" );
|
|
enemy endon( "death" );
|
|
|
|
if ( !isdefined( self.fakeTargetEnt ) )
|
|
{
|
|
self.fakeTargetEnt = Spawn( "script_origin", point );
|
|
}
|
|
|
|
self.fakeTargetEnt Unlink();
|
|
|
|
self.fakeTargetEnt.origin = point;
|
|
self SetTurretTargetEnt( self.fakeTargetEnt );
|
|
self waittill( "turret_on_target" );
|
|
|
|
timeStart = GetTime();
|
|
offset = (0, 0, 0);
|
|
if( IsSentient( enemy ) )
|
|
{
|
|
offset = enemy GetEye() - enemy.origin;
|
|
}
|
|
|
|
while( GetTime() < timeStart + timeToHit * 1000 )
|
|
{
|
|
self.fakeTargetEnt.origin = LerpVector( point, enemy.origin + offset, ( GetTime() - timeStart ) / ( timeToHit * 1000 ) );
|
|
///#debugstar( self.fakeTargetEnt.origin, 1000, (0,1,0) ); #/
|
|
{wait(.05);};
|
|
}
|
|
|
|
self.fakeTargetEnt.origin = enemy.origin + offset;
|
|
{wait(.05);};
|
|
self.fakeTargetEnt LinkTo( enemy );
|
|
}
|
|
|
|
function Attack_Thread_MainTurret()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self endon( "end_attack_thread" );
|
|
|
|
while( 1 )
|
|
{
|
|
enemy = self.enemy;
|
|
if( isdefined( enemy ) )
|
|
{
|
|
self SetLookAtEnt( enemy );
|
|
|
|
if( self VehCanSee( enemy ) )
|
|
{
|
|
vectorFromEnemy = VectorNormalize( ( (self.origin - enemy.origin)[0], (self.origin - enemy.origin)[1], 0 ) );
|
|
|
|
self thread Delay_Target_ToEnemy_Thread( enemy.origin + vectorFromEnemy * 300, enemy, 1.5 );
|
|
|
|
self waittill( "turret_on_target" );
|
|
self vehicle_ai::fire_for_time( 2 + RandomFloat( 0.8 ) );
|
|
|
|
self ClearTurretTarget();
|
|
self SetTurretTargetRelativeAngles( (15,0,0), 0 );
|
|
|
|
if( isdefined( enemy ) && IsAI( enemy ) )
|
|
{
|
|
wait( 2.5 + RandomFloat( 0.5 ) );
|
|
}
|
|
else
|
|
{
|
|
wait( 2.0 + RandomFloat( 0.4 ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
wait 0.4;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self ClearTurretTarget();
|
|
self ClearLookAtEnt();
|
|
wait 0.4;
|
|
}
|
|
}
|
|
}
|
|
|
|
function Attack_Thread_rocket()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self endon( "end_attack_thread" );
|
|
|
|
while( true )
|
|
{
|
|
enemy = self.enemy;
|
|
if ( !isdefined( enemy ) )
|
|
{
|
|
wait 1;
|
|
continue;
|
|
}
|
|
|
|
if ( isdefined( enemy ) && self VehCanSee( enemy ) && vehicle_ai::IsCooldownReady( "rocket_launcher" ) )
|
|
{
|
|
vehicle_ai::Cooldown( "rocket_launcher", 8 );
|
|
|
|
self notify( "end_movement_thread" );
|
|
self ClearVehGoalPos();
|
|
self SetVehGoalPos( self.origin, true, false );
|
|
|
|
target = enemy.origin;
|
|
self SetLookAtEnt( enemy );
|
|
self hunter_lockon_fx();
|
|
|
|
wait 1.5;
|
|
|
|
eye = self GetTagOrigin( "tag_eye" );
|
|
if ( isdefined( enemy ) )
|
|
{
|
|
anglesToTarget = VectorToAngles( enemy.origin - eye );
|
|
angles = anglesToTarget - self.angles;
|
|
if ( -30 < angles[0] && angles[0] < 60 && -70 < angles[1] && angles[1] < 70 )
|
|
{
|
|
target = enemy.origin;
|
|
}
|
|
else
|
|
{
|
|
anglesToTarget = VectorToAngles( target - eye );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
anglesToTarget = VectorToAngles( target - eye );
|
|
}
|
|
|
|
rightDir = AnglesToRight( anglesToTarget );
|
|
|
|
randomRange = 30;
|
|
offset = [];
|
|
offset[0] = -rightDir * randomRange * 2 + ( RandomFloatRange( -randomRange, randomRange ), RandomFloatRange( -randomRange, randomRange ), 0 );
|
|
offset[1] = rightDir * randomRange * 2 + ( RandomFloatRange( -randomRange, randomRange ), RandomFloatRange( -randomRange, randomRange ), 0 );
|
|
|
|
self hunter_fire_one_missile( 0, target, offset[0] );
|
|
|
|
wait 0.5;
|
|
|
|
if ( isdefined( enemy ) )
|
|
{
|
|
eye = self GetTagOrigin( "tag_eye" );
|
|
angles = VectorToAngles( enemy.origin - eye ) - self.angles;
|
|
if ( -30 < angles[0] && angles[0] < 60 && -70 < angles[1] && angles[1] < 70 )
|
|
{
|
|
target = enemy.origin;
|
|
}
|
|
}
|
|
self hunter_fire_one_missile( 1, target, offset[1] );
|
|
|
|
wait 1;
|
|
|
|
self thread Movement_Thread_StayInDistance();
|
|
}
|
|
wait 0.5;
|
|
}
|
|
}
|
|
// ----------------------------------------------
|
|
|
|
// best target of side turrets: closest, can hit, and not target of main turret or other turret
|
|
function side_turret_get_best_target( a_potential_targets, n_index )
|
|
{
|
|
if ( self.ignoreall === true )
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
shouldYield = true && level.gameskill < 3;
|
|
|
|
main_turret_target = self.enemy;
|
|
|
|
if ( n_index === 2 )
|
|
{
|
|
other_turret_target = turret::get_target( 1 );
|
|
}
|
|
|
|
if ( shouldYield )
|
|
{
|
|
ArrayRemoveValue( a_potential_targets, main_turret_target );
|
|
ArrayRemoveValue( a_potential_targets, other_turret_target );
|
|
}
|
|
|
|
e_best_target = undefined;
|
|
|
|
while ( !isdefined( e_best_target ) && ( a_potential_targets.size > 0 ) )
|
|
{
|
|
e_closest_target = ArrayGetClosest( self.origin, a_potential_targets );
|
|
|
|
if( self turret::can_hit_target( e_closest_target, n_index ) )
|
|
{
|
|
e_best_target = e_closest_target;
|
|
}
|
|
else
|
|
{
|
|
ArrayRemoveValue( a_potential_targets, e_closest_target );
|
|
}
|
|
}
|
|
|
|
return e_best_target;
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// missile
|
|
// ----------------------------------------------
|
|
function hunter_fire_one_missile( launcher_index, target, offset, blinkLights, waittimeAfterBlinkLights )
|
|
{
|
|
self endon( "death" );
|
|
|
|
if ( ( isdefined( blinkLights ) && blinkLights ) )
|
|
{
|
|
self vehicle_ai::blink_lights_for_time( 1 );
|
|
|
|
if ( isdefined( waittimeAfterBlinkLights ) && waittimeAfterBlinkLights > 0 )
|
|
{
|
|
wait waittimeAfterBlinkLights;
|
|
}
|
|
}
|
|
|
|
if ( !isdefined( offset ) )
|
|
{
|
|
offset = ( 0, 0, 0 );
|
|
}
|
|
|
|
spawnTag = self.missileTags[ launcher_index ];
|
|
origin = self GetTagOrigin( spawnTag );
|
|
angles = self GetTagAngles( spawnTag );
|
|
forward = AnglesToForward( angles );
|
|
up = AnglesToUp( angles );
|
|
|
|
if ( isdefined( spawnTag ) && isdefined( target ) )
|
|
{
|
|
weapon = GetWeapon( "hunter_rocket_turret" );
|
|
|
|
//only do the full MagicBullet parameter list if target is a real entity, and not a script_struct
|
|
if ( IsEntity( target ) )
|
|
{
|
|
missile = MagicBullet( weapon, origin, target.origin + offset, self, target, offset );
|
|
//missile thread remote_missile_life();
|
|
}
|
|
else if ( IsVec( target ) )
|
|
{
|
|
missile = MagicBullet( weapon, origin, target + offset, self );
|
|
}
|
|
else
|
|
{
|
|
missile = MagicBullet( weapon, origin, target.origin + offset, self );
|
|
}
|
|
}
|
|
}
|
|
|
|
function remote_missile_life()
|
|
{
|
|
self endon( "death" );
|
|
|
|
hostmigration::waitLongDurationWithHostMigrationPause( 10 );
|
|
|
|
playFX( level.remote_mortar_fx["missileExplode"], self.origin );
|
|
self playlocalsound( "mpl_ks_reaper_explosion" );
|
|
self delete();
|
|
}
|
|
|
|
function hunter_lockon_fx()
|
|
{
|
|
self thread vehicle_ai::blink_lights_for_time( 1.5 );
|
|
self playsound( "veh_hunter_alarm_target" );
|
|
}
|
|
|
|
//self == hunter
|
|
function getEnemyArray( include_ai, include_player )
|
|
{
|
|
enemyArray = [];
|
|
|
|
enemy_team = "allies";//util::getOtherTeam( self.team );
|
|
|
|
if ( ( isdefined( include_ai ) && include_ai ) )
|
|
{
|
|
aiArray = GetAITeamArray( enemy_team );
|
|
enemyArray = ArrayCombine( enemyArray, aiArray, false, false );
|
|
}
|
|
|
|
if ( ( isdefined( include_player ) && include_player ) )
|
|
{
|
|
playerArray = GetPlayers( enemy_team );
|
|
enemyArray = ArrayCombine( enemyArray, playerArray, false, false );
|
|
}
|
|
|
|
return enemyArray;
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// scanner
|
|
// ----------------------------------------------
|
|
|
|
//self == hunter
|
|
function is_point_in_view( point, do_trace )
|
|
{
|
|
if ( !isdefined( point ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
scanner = self.frontScanner;
|
|
vector_to_point = point - scanner.origin;
|
|
in_view = ( LengthSquared( vector_to_point ) <= ( (10000) * (10000) ) );
|
|
|
|
if ( in_view )
|
|
{
|
|
in_view = util::within_fov( scanner.origin, scanner.angles, point, Cos( 190 ) );
|
|
}
|
|
|
|
if ( in_view && ( isdefined( do_trace ) && do_trace ) && isdefined( self.enemy ) )
|
|
{
|
|
in_view = SightTracePassed( scanner.origin, point, false, self.enemy );
|
|
}
|
|
|
|
return in_view;
|
|
}
|
|
|
|
//self == hunter
|
|
function is_valid_target( target, do_trace )
|
|
{
|
|
target_is_valid = true;
|
|
|
|
// check script properties
|
|
if ( ( isdefined( target.ignoreme ) && target.ignoreme ) || ( target.health <= 0 ) )
|
|
{
|
|
target_is_valid = false;
|
|
}
|
|
// check sentient properties
|
|
else if ( IsSentient( target ) && ( ( target IsNoTarget() ) || ( target ai::is_dead_sentient() ) ) )
|
|
{
|
|
target_is_valid = false;
|
|
}
|
|
// check fov
|
|
else if ( isdefined( target.origin ) && !is_point_in_view( target.origin, do_trace ) )
|
|
{
|
|
target_is_valid = false;
|
|
}
|
|
|
|
return target_is_valid;
|
|
}
|
|
|
|
//self == hunter
|
|
function get_enemies_in_view( do_trace )
|
|
{
|
|
validEnemyArray = [];
|
|
enemyArray = getEnemyArray( true, true );
|
|
|
|
foreach( enemy in enemyArray )
|
|
{
|
|
if ( is_valid_target( enemy, do_trace ) )
|
|
{
|
|
if ( !isdefined( validEnemyArray ) ) validEnemyArray = []; else if ( !IsArray( validEnemyArray ) ) validEnemyArray = array( validEnemyArray ); validEnemyArray[validEnemyArray.size]=enemy;;
|
|
}
|
|
}
|
|
|
|
return validEnemyArray;
|
|
}
|
|
|
|
// self == hunter
|
|
function hunter_scanner_init()
|
|
{
|
|
self.frontScanner = Spawn( "script_model", self GetTagOrigin( "tag_gunner_flash3" ) );
|
|
self.frontScanner SetModel( "tag_origin" );
|
|
|
|
self.frontScanner.angles = self GetTagAngles( "tag_gunner_flash3" );
|
|
self.frontScanner LinkTo( self, "tag_gunner_flash3" );
|
|
|
|
self.frontScanner.owner = self;
|
|
self.frontScanner.hasTargetEnt = false;
|
|
|
|
self.frontScanner.sndScanningEnt = spawn( "script_origin", self.frontScanner.origin + anglesToForward( self.angles ) * 1000 );
|
|
self.frontScanner.sndScanningEnt linkto( self.frontScanner );
|
|
|
|
wait 0.25;
|
|
|
|
//self.frontScanner thread hunter_scanner_lookTarget( self );
|
|
|
|
if ( false )
|
|
{
|
|
PlayFxOnTag( self.settings.spotlightfx, self.frontScanner, "tag_origin" );
|
|
}
|
|
}
|
|
|
|
// self == hunter
|
|
function hunter_scanner_SetTargetEntity( targetEnt, offset )
|
|
{
|
|
if ( !isdefined( offset ) )
|
|
{
|
|
offset = ( 0, 0, 0 );
|
|
}
|
|
|
|
if( IsDefined( targetEnt ) )
|
|
{
|
|
self.frontScanner.targetEnt = targetEnt;
|
|
self.frontScanner.hasTargetEnt = true;
|
|
self SetGunnerTargetEnt( self.frontScanner.targetEnt, offset, 2 );
|
|
}
|
|
}
|
|
|
|
// self == hunter
|
|
function hunter_scanner_ClearLookTarget()
|
|
{
|
|
self.frontScanner.hasTargetEnt = false;
|
|
self ClearGunnerTarget( 2 );
|
|
}
|
|
|
|
// self == hunter
|
|
function hunter_scanner_SetTargetPosition( targetPos )
|
|
{
|
|
if( IsDefined( targetPos ) )
|
|
{
|
|
self.frontScanner.targetPos = targetPos;
|
|
self SetGunnerTargetVec( self.frontScanner.targetPos, 2 );
|
|
}
|
|
}
|
|
|
|
function hunter_frontScanning()
|
|
{
|
|
self endon( "death_shut_off" );
|
|
self endon( "crash_done" );
|
|
self endon( "death" );
|
|
|
|
hunter_scanner_init();
|
|
|
|
offsetFactorPitch = 0;
|
|
offsetFactorYaw = 0;
|
|
|
|
// use 2 different irrational numbers here to help avoiding repetitive patterns
|
|
pitchStep = 1 * 2.23606797749978969640; // irrational number sqrt(5)
|
|
yawStep = 1 * 3.14159265358979323846; // irrational number PI
|
|
|
|
pitchRange = 20;
|
|
yawRange = 45;
|
|
|
|
scannerDirection = undefined;
|
|
|
|
while ( 1 )
|
|
{
|
|
scannerOrigin = self.frontScanner.origin;
|
|
|
|
if ( ( isdefined( self.inpain ) && self.inpain ) )
|
|
{
|
|
wait 0.3;
|
|
offset = ( 50, 0, 0 ) + ( math::randomSign() * RandomFloatRange( 1, 2 ) * pitchRange, math::randomSign() * RandomFloatRange( 1, 2 ) * yawRange, 0 );
|
|
scannerDirection = anglesToForward( self.angles + offset );
|
|
}
|
|
else if ( !IsDefined( self.enemy ) )
|
|
{
|
|
if ( false )
|
|
{
|
|
self.frontScanner.sndScanningEnt playloopsound( "veh_hunter_scanner_loop", 1 );
|
|
}
|
|
|
|
offsetFactorPitch = offsetFactorPitch + pitchStep;
|
|
offsetFactorYaw = offsetFactorYaw + yawStep;
|
|
|
|
offset = ( 50, 0, 0 ) + ( Sin( offsetFactorPitch ) * pitchRange, Cos( offsetFactorYaw ) * yawRange, 0 );
|
|
scannerDirection = anglesToForward( self.angles + offset );
|
|
|
|
enemies = get_enemies_in_view( true );
|
|
|
|
if ( enemies.size > 0 )
|
|
{
|
|
closest_enemy = ArrayGetClosest( self.origin, enemies );
|
|
|
|
self.favoriteEnemy = closest_enemy;
|
|
/# line( scannerOrigin, closest_enemy.origin, ( 0, 1, 0 ), 1, 3 ); #/
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( self is_point_in_view( self.enemy.origin, true ) )
|
|
{
|
|
self notify ( "hunter_lockOnTargetInSight" );
|
|
}
|
|
else
|
|
{
|
|
self notify ( "hunter_lockOnTargetOutSight" );
|
|
}
|
|
|
|
scannerDirection = VectorNormalize( self.enemy.origin - scannerOrigin );
|
|
|
|
if ( false )
|
|
{
|
|
self.frontScanner.sndScanningEnt stoploopsound( 1 );
|
|
}
|
|
}
|
|
|
|
targetLocation = scannerOrigin + scannerDirection * 1000; // any big value will do
|
|
self hunter_scanner_SetTargetPosition( targetLocation );
|
|
|
|
/# line( scannerOrigin, self.frontScanner.targetPos, ( 0, 1, 0 ), 1, 1000 ); #/
|
|
wait 0.1;
|
|
}
|
|
}
|
|
|
|
function hunter_exit_vehicle()
|
|
{
|
|
self waittill( "exit_vehicle", player );
|
|
|
|
player.ignoreme = false;
|
|
player DisableInvulnerability();
|
|
|
|
self SetHeliHeightLock( false );
|
|
self EnableAimAssist();
|
|
self SetVehicleType( self.original_vehicle_type );
|
|
self.attachedpath = undefined;
|
|
self SetGoal( self.origin, false, 4096, 512 );
|
|
}
|
|
|
|
function hunter_scripted( params )
|
|
{
|
|
// do nothing state
|
|
params.driver = self GetSeatOccupant( 0 );
|
|
if ( isdefined( params.driver ) )
|
|
{
|
|
self DisableAimAssist();
|
|
|
|
self thread vehicle_death::vehicle_damage_filter( "firestorm_turret" );
|
|
//self thread hunter_set_team( driver.team );
|
|
params.driver.ignoreme = true;
|
|
params.driver EnableInvulnerability();
|
|
|
|
if ( isdefined( self.vehicle_weapon_override ) )
|
|
{
|
|
self SetVehWeapon( self.vehicle_weapon_override );
|
|
}
|
|
|
|
self thread hunter_exit_vehicle();
|
|
//self thread hunter_update_rumble();
|
|
self thread hunter_collision_player();
|
|
//self thread hunter_self_destruct();
|
|
self thread player_fire_update_side_turret_1();
|
|
self thread player_fire_update_side_turret_2();
|
|
self thread player_fire_update_rocket();
|
|
}
|
|
|
|
if ( isdefined( self.goal_node ) && isdefined( self.goal_node.hunter_claimed ) )
|
|
{
|
|
self.goal_node.hunter_claimed = undefined;
|
|
}
|
|
|
|
self ClearTargetEntity();
|
|
self ClearVehGoalPos();
|
|
self PathVariableOffsetClear();
|
|
self PathFixedOffsetClear();
|
|
self ClearLookAtEnt();
|
|
self ResumeSpeed();
|
|
}
|
|
|
|
function player_fire_update_side_turret_1()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "exit_vehicle" );
|
|
|
|
weapon = self SeatGetWeapon( 1 );
|
|
fireTime = weapon.fireTime;
|
|
|
|
while( 1 )
|
|
{
|
|
self SetGunnerTargetVec( self GetTurretTargetVec( 0 ), 0 );
|
|
if( self IsDriverFiring( ) )
|
|
{
|
|
self FireWeapon( 1 );
|
|
}
|
|
wait fireTime;
|
|
}
|
|
}
|
|
|
|
function player_fire_update_side_turret_2()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "exit_vehicle" );
|
|
|
|
weapon = self SeatGetWeapon( 2 );
|
|
fireTime = weapon.fireTime;
|
|
|
|
while( 1 )
|
|
{
|
|
self SetGunnerTargetVec( self GetTurretTargetVec( 0 ), 1 );
|
|
if( self IsDriverFiring( ) )
|
|
{
|
|
self FireWeapon( 2 );
|
|
}
|
|
wait fireTime;
|
|
}
|
|
}
|
|
|
|
function player_fire_update_rocket()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "exit_vehicle" );
|
|
|
|
weapon = GetWeapon( "hunter_rocket_turret_player" );
|
|
fireTime = weapon.fireTime;
|
|
driver = self GetSeatOccupant( 0 );
|
|
|
|
while( 1 )
|
|
{
|
|
|
|
if( driver ButtonPressed( "BUTTON_A" ) )
|
|
{
|
|
spawnTag0 = self.missileTags[ 0 ];
|
|
spawnTag1 = self.missileTags[ 1 ];
|
|
origin0 = self GetTagOrigin( spawnTag0 );
|
|
origin1 = self GetTagOrigin( spawnTag1 );
|
|
target = self GetTurretTargetVec( 0 );
|
|
|
|
MagicBullet( weapon, origin0, target );
|
|
MagicBullet( weapon, origin1, target );
|
|
|
|
wait fireTime;
|
|
}
|
|
|
|
Wait 0.05;
|
|
}
|
|
}
|
|
|
|
function hunter_collision_player()
|
|
{
|
|
self endon( "change_state" );
|
|
self endon( "crash_done" );
|
|
self endon( "death" );
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "veh_collision", velocity, normal );
|
|
driver = self GetSeatOccupant( 0 );
|
|
if ( isdefined( driver ) && LengthSquared( velocity ) > 70 * 70 )
|
|
{
|
|
Earthquake( 0.25, 0.25, driver.origin, 50 );
|
|
driver PlayRumbleOnEntity( "damage_heavy" );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lots of gross hardcoded values! :(
|
|
function hunter_update_rumble()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "exit_vehicle" );
|
|
|
|
while ( 1 )
|
|
{
|
|
vr = Abs( self GetSpeed() / self GetMaxSpeed() );
|
|
|
|
if ( vr < 0.1 )
|
|
{
|
|
level.player PlayRumbleOnEntity( "hunter_fly" );
|
|
wait 0.35;
|
|
}
|
|
else
|
|
{
|
|
time = RandomFloatRange( 0.1, 0.2 );
|
|
Earthquake( RandomFloatRange( 0.1, 0.15 ), time, self.origin, 200 );
|
|
level.player PlayRumbleOnEntity( "hunter_fly" );
|
|
wait time;
|
|
}
|
|
}
|
|
}
|
|
|
|
function hunter_self_destruct()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "exit_vehicle" );
|
|
|
|
const max_self_destruct_time = 5;
|
|
|
|
self_destruct = false;
|
|
self_destruct_time = 0;
|
|
|
|
while ( 1 )
|
|
{
|
|
if ( !self_destruct )
|
|
{
|
|
if ( level.player MeleeButtonPressed() )
|
|
{
|
|
self_destruct = true;
|
|
self_destruct_time = max_self_destruct_time;
|
|
}
|
|
|
|
{wait(.05);};
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
IPrintLnBold( self_destruct_time );
|
|
|
|
wait 1;
|
|
|
|
self_destruct_time -= 1;
|
|
if ( self_destruct_time == 0 )
|
|
{
|
|
driver = self GetSeatOccupant( 0 );
|
|
if ( isdefined( driver ) )
|
|
{
|
|
driver DisableInvulnerability();
|
|
}
|
|
|
|
Earthquake( 3, 1, self.origin, 256 );
|
|
RadiusDamage( self.origin, 1000, 15000, 15000, level.player, "MOD_EXPLOSIVE" );
|
|
self DoDamage( self.health + 1000, self.origin );
|
|
}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
function hunter_level_out_for_landing()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "emped" );
|
|
self endon( "landed" );
|
|
|
|
while ( isdefined( self.emped ) )
|
|
{
|
|
velocity = self.velocity; // setting the angles clears the velocity so we save it off and set it back
|
|
self.angles = ( self.angles[0] * 0.85, self.angles[1], self.angles[2] * 0.85 );
|
|
ang_vel = self GetAngularVelocity() * 0.85;
|
|
self SetAngularVelocity( ang_vel );
|
|
self SetVehVelocity( velocity );
|
|
{wait(.05);};
|
|
}
|
|
}
|
|
|
|
function hunter_emped( params )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "emped" );
|
|
|
|
self.emped = true;
|
|
|
|
wait RandomFloatRange( 4, 7 );
|
|
|
|
self vehicle_ai::evaluate_connections();
|
|
|
|
/*
|
|
PlaySoundAtPosition( "veh_qrdrone_emp_down", self.origin );
|
|
|
|
if ( !isdefined( self.stun_fx ) )
|
|
{
|
|
self.stun_fx = Spawn( "script_model", self.origin );
|
|
self.stun_fx SetModel( "tag_origin" );
|
|
self.stun_fx LinkTo( self, "tag_origin", ( 0, 0, 0 ), ( 0, 0, 0 ) );
|
|
PlayFXOnTag( level._effect[ "hunter_stun" ], self.stun_fx, "tag_origin" );
|
|
}
|
|
|
|
wait RandomFloatRange( 4, 7 );
|
|
|
|
self.stun_fx delete();
|
|
|
|
self playsound ( "veh_qrdrone_boot_qr" );
|
|
*/
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// pain/hit reaction
|
|
// ----------------------------------------------
|
|
function hunter_pain_for_time( time, velocityStablizeParam, rotationStablizeParam, restoreLookPoint )
|
|
{
|
|
self endon( "death" );
|
|
self.painStartTime = GetTime();
|
|
|
|
if ( !( isdefined( self.inpain ) && self.inpain ) )
|
|
{
|
|
self.inpain = true;
|
|
|
|
while ( GetTime() < self.painStartTime + time * 1000 )
|
|
{
|
|
self SetVehVelocity( self.velocity * velocityStablizeParam );
|
|
self SetAngularVelocity( self GetAngularVelocity() * rotationStablizeParam );
|
|
wait 0.1;
|
|
}
|
|
|
|
if ( isdefined( restoreLookPoint ) )
|
|
{
|
|
restoreLookEnt = Spawn( "script_model", restoreLookPoint );
|
|
restoreLookEnt SetModel( "tag_origin" );
|
|
|
|
self ClearLookAtEnt();
|
|
self SetLookAtEnt( restoreLookEnt );
|
|
self setTurretTargetEnt( restoreLookEnt );
|
|
wait 1.5;
|
|
|
|
self ClearLookAtEnt();
|
|
self ClearTurretTarget();
|
|
restoreLookEnt delete();
|
|
}
|
|
|
|
self.inpain = false;
|
|
}
|
|
}
|
|
|
|
function hunter_pain_small( eAttacker, damageType, hitPoint, hitDirection, hitLocationInfo, partName )
|
|
{
|
|
if( !isdefined( hitPoint ) || !isdefined( hitDirection ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
self SetVehVelocity( self.velocity + VectorNormalize( hitDirection ) * 20 );
|
|
|
|
if ( !( isdefined( self.inpain ) && self.inpain ) )
|
|
{
|
|
vecRight = anglesToRight( self.angles );
|
|
sign = math::sign( vectorDot( vecRight, hitDirection ) );
|
|
yaw_vel = sign * RandomFloatRange( 100, 140 );
|
|
|
|
ang_vel = self GetAngularVelocity();
|
|
ang_vel += ( RandomFloatRange( -120, -100 ), yaw_vel, RandomFloatRange( -100, 100 ) );
|
|
self SetAngularVelocity( ang_vel );
|
|
|
|
self thread hunter_pain_for_time( 1.5, 1.0, 0.8 );
|
|
}
|
|
|
|
self vehicle_ai::set_state( "strafe" );
|
|
}
|
|
|
|
function HunterCallback_VehicleDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, weapon, vPoint, vDir, sHitLoc, vDamageOrigin, psOffsetTime, damageFromUnderneath, modelIndex, partName, vSurfaceNormal )
|
|
{
|
|
driver = self GetSeatOccupant( 0 );
|
|
|
|
// no friendly fire
|
|
if ( isdefined( eAttacker ) && eAttacker.team == self.team )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
num_players = GetPlayers().size;
|
|
maxDamage = self.healthdefault * ( 0.35 - 0.025 * num_players );
|
|
if ( sMeansOfDeath !== "MOD_UNKNOWN" && iDamage > maxDamage )
|
|
{
|
|
iDamage = maxDamage;
|
|
}
|
|
|
|
if ( !isdefined( self.damageLevel ) )
|
|
{
|
|
self.damageLevel = 0;
|
|
self.newDamageLevel = self.damageLevel;
|
|
}
|
|
|
|
newDamageLevel = vehicle::should_update_damage_fx_level( self.health, iDamage, self.healthdefault );
|
|
if ( newDamageLevel > self.damageLevel )
|
|
{
|
|
self.newDamageLevel = newDamageLevel;
|
|
}
|
|
|
|
if ( self.newDamageLevel > self.damageLevel )
|
|
{
|
|
self.damageLevel = self.newDamageLevel;
|
|
if ( self.pain_when_damagelevel_change === true )
|
|
{
|
|
hunter_pain_small( eAttacker, sMeansOfDeath, vPoint, vDir, sHitLoc, partName );
|
|
}
|
|
vehicle::set_damage_fx_level( self.damageLevel );
|
|
}
|
|
|
|
if ( vehicle_ai::should_emp( self, weapon, sMeansOfDeath, eInflictor, eAttacker ) )
|
|
{
|
|
hunter_pain_small( eAttacker, sMeansOfDeath, vPoint, vDir, sHitLoc, partName );
|
|
}
|
|
|
|
return iDamage;
|
|
}
|