2331 lines
62 KiB
Plaintext
2331 lines
62 KiB
Plaintext
#using scripts\codescripts\struct;
|
|
|
|
#using scripts\shared\flag_shared;
|
|
#using scripts\shared\math_shared;
|
|
#using scripts\shared\array_shared;
|
|
#using scripts\shared\sound_shared;
|
|
#using scripts\shared\system_shared;
|
|
#using scripts\shared\util_shared;
|
|
#using scripts\shared\statemachine_shared;
|
|
#using scripts\shared\vehicle_shared;
|
|
#using scripts\shared\vehicle_death_shared;
|
|
#using scripts\shared\turret_shared;
|
|
#using scripts\shared\ai\systems\ai_interface;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#namespace vehicle_ai;
|
|
|
|
function autoexec __init__sytem__() { system::register("vehicle_ai",&__init__,undefined,undefined); }
|
|
|
|
// _vehicle_ai.gsc - all things related to vehicles ai
|
|
|
|
#using_animtree( "generic" );
|
|
|
|
function __init__()
|
|
{
|
|
}
|
|
|
|
function RegisterSharedInterfaceAttributes( archetype )
|
|
{
|
|
/*
|
|
* Name: force_high_speed
|
|
* Summary: Always uses max speed instead of default speed when moving
|
|
* Example: ai::SetAiAttribute( vehicle, "sprint", true );"
|
|
*/
|
|
ai::RegisterMatchedInterface(
|
|
archetype,
|
|
"force_high_speed",
|
|
false,
|
|
array( true, false ) );
|
|
}
|
|
|
|
//*****************************************************************************
|
|
//
|
|
// Vehicle AI Commands
|
|
//
|
|
// - These are used in conjunction with the vehicle ai system
|
|
// - If the vehicle is not setup to use the ai system they might do nothing
|
|
// - Please see James Snider or Ruoyao Ma if you have questions
|
|
//
|
|
//*****************************************************************************
|
|
|
|
function InitThreatBias()
|
|
{
|
|
aiarray = GetAIArray();
|
|
|
|
foreach( ai in aiarray )
|
|
{
|
|
if ( ai === self )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( self.ignoreFireFly === true && ai.firefly === true )
|
|
{
|
|
self SetPersonalIgnore( ai );
|
|
}
|
|
|
|
if ( self.ignoreDecoy === true && ai.isDecoy === true )
|
|
{
|
|
self SetPersonalIgnore( ai );
|
|
}
|
|
}
|
|
}
|
|
|
|
function EntityIsArchetype( entity, archetype )
|
|
{
|
|
if ( !isdefined( entity ) )
|
|
{
|
|
return false;
|
|
}
|
|
if ( isplayer( entity ) && entity.usingvehicle && isdefined( entity.viewlockedentity ) && entity.viewlockedentity.archetype === archetype )
|
|
{
|
|
return true;
|
|
}
|
|
if ( isvehicle( entity ) && entity.archetype === archetype )
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// self == AI vehicle
|
|
function GetEnemyTarget()
|
|
{
|
|
if( isdefined( self.enemy ) && self VehCanSee( self.enemy ) )
|
|
{
|
|
return self.enemy;
|
|
}
|
|
else if( isdefined( self.enemylastseenpos ) )
|
|
{
|
|
return self.enemylastseenpos;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function GetTargetPos( target, geteye )
|
|
{
|
|
pos = undefined;
|
|
|
|
if ( isdefined( target ) )
|
|
{
|
|
if ( IsVec( target ) )
|
|
{
|
|
pos = target;
|
|
}
|
|
else if ( ( isdefined( geteye ) && geteye ) && IsSentient( target ) )
|
|
{
|
|
pos = target GetEye();
|
|
}
|
|
else if ( IsEntity( target ) )
|
|
{
|
|
pos = target.origin;
|
|
}
|
|
else if ( isdefined( target.origin ) && IsVec( target.origin ) )
|
|
{
|
|
pos = target.origin;
|
|
}
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
function GetTargetEyeOffset( target )
|
|
{
|
|
offset = (0,0,0);
|
|
|
|
if ( isdefined( target ) && IsSentient( target ) )
|
|
{
|
|
offset = target GetEye() - target.origin;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
/@
|
|
"Name: fire_for_time( <totalFireTime>, [turretIdx], [target] )"
|
|
"Summary: fire a weapon for a set of period"
|
|
"CallOn: a vehicle"
|
|
"MandatoryArg: <totalFireTime> : how long should the weapon fire"
|
|
"OptionalArg: [turretIdx] : which weapon to use. use number to get turret on vehicle. if undefined the vehicle will try to use it's main weapon."
|
|
"OptionalArg: [target] : what target it is shooting at. this will affect hit chance."
|
|
"Example: vehicle vehicle_ai::fire_for_time( RandomFloatRange( 0.3, 0.6 ) ); "
|
|
@/
|
|
function fire_for_time( totalFireTime, turretIdx, target, intervalScale = 1.0 )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self notify( "fire_stop" );
|
|
self endon( "fire_stop" );
|
|
|
|
if ( !isdefined( turretIdx ) )
|
|
{
|
|
turretIdx = 0;
|
|
}
|
|
|
|
weapon = self SeatGetWeapon( turretIdx );
|
|
|
|
assert( isdefined( weapon ) && weapon.name!="none" && weapon.fireTime>0 );
|
|
|
|
firetime = weapon.firetime * intervalScale;
|
|
|
|
fireCount = int( floor( totalFireTime / fireTime ) ) + 1;
|
|
|
|
__fire_for_rounds_internal( fireCount, fireTime, turretIdx, target );
|
|
}
|
|
|
|
/@
|
|
"Name: fire_for_rounds( <fireCount>, [turretIdx], [target] )"
|
|
"Summary: fire a weapon for a set of period"
|
|
"CallOn: a vehicle"
|
|
"MandatoryArg: <fireCount> : how many rounds should the weapon fire"
|
|
"OptionalArg: [turretIdx] : which weapon to use. use number to get turret on vehicle. if undefined the vehicle will try to use it's main weapon."
|
|
"OptionalArg: [target] : what target it is shooting at. this will affect hit chance."
|
|
"Example: vehicle vehicle_ai::fire_for_rounds( 5 ); "
|
|
@/
|
|
function fire_for_rounds( fireCount, turretIdx, target )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "fire_stop" );
|
|
self endon( "change_state" );
|
|
|
|
if ( !isdefined( turretIdx ) )
|
|
{
|
|
turretIdx = 0;
|
|
}
|
|
|
|
weapon = self SeatGetWeapon( turretIdx );
|
|
|
|
assert( isdefined( weapon ) && weapon.name!="none" && weapon.fireTime>0 );
|
|
|
|
__fire_for_rounds_internal( fireCount, weapon.fireTime, turretIdx, target );
|
|
}
|
|
|
|
function __fire_for_rounds_internal( fireCount, fireInterval, turretIdx, target )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "fire_stop" );
|
|
self endon( "change_state" );
|
|
|
|
if( isdefined( target ) && IsSentient( target ) )
|
|
{
|
|
target endon( "death" );
|
|
}
|
|
|
|
assert( isdefined( turretIdx ) );
|
|
|
|
aiFireChance = 1;
|
|
|
|
// fire less, unless shooting player or a bigdog or mentioned specifically from level script
|
|
if ( ( isdefined( target ) && !IsPlayer( target ) && isAI( target ) ) || isdefined( self.fire_half_blanks ) )
|
|
{
|
|
// 1 in 2 bullets will be real
|
|
aiFireChance = 2;
|
|
}
|
|
|
|
counter = 0;
|
|
while ( counter < fireCount )
|
|
{
|
|
if ( self.avoid_shooting_owner === true && self owner_in_line_of_fire() )
|
|
{
|
|
wait fireInterval;
|
|
continue;
|
|
}
|
|
|
|
if ( isdefined( target ) && !isVec( target ) && isdefined( target.attackerAccuracy ) && target.attackerAccuracy == 0 )
|
|
{
|
|
self FireTurret( turretIdx, true );
|
|
}
|
|
else if ( aiFireChance > 1 )
|
|
{
|
|
self FireTurret( turretIdx, counter % aiFireChance );
|
|
}
|
|
else
|
|
{
|
|
self FireTurret( turretIdx );
|
|
}
|
|
|
|
counter++;
|
|
wait fireInterval;
|
|
}
|
|
}
|
|
|
|
function owner_in_line_of_fire()
|
|
{
|
|
if ( !isdefined( self.owner ) )
|
|
return false;
|
|
|
|
dist_squared_to_owner = DistanceSquared( self.owner.origin, self.origin );
|
|
line_of_fire_dot = ( ( dist_squared_to_owner < 96 * 96 ) ? 0.8660 : 0.9848 ); // cos( 30 degrees ) : cos ( 10 degrees );
|
|
gun_angles = self GetTagAngles( (isdefined(self.avoid_shooting_owner_ref_tag)?self.avoid_shooting_owner_ref_tag:"tag_flash") );
|
|
gun_forward = AnglesToForward( gun_angles );
|
|
dot = VectorDot( gun_forward, VectorNormalize( self.owner.origin - self.origin ) ); // rough approximation, good enough
|
|
return ( dot > line_of_fire_dot );
|
|
}
|
|
|
|
function SetTurretTarget( target, turretIdx = 0, offset = (0,0,0) )
|
|
{
|
|
if ( isEntity( target ) )
|
|
{
|
|
if ( turretIdx == 0 )
|
|
{
|
|
self SetTurretTargetEnt( target, offset );
|
|
}
|
|
else
|
|
{
|
|
self SetGunnerTargetEnt( target, offset, turretIdx - 1 );
|
|
}
|
|
}
|
|
else if ( isVec( target ) )
|
|
{
|
|
origin = target + offset;
|
|
|
|
if ( turretIdx == 0 )
|
|
{
|
|
self SetTurretTargetVec( target );
|
|
}
|
|
else
|
|
{
|
|
self SetGunnerTargetVec( target, turretIdx - 1 );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AssertMsg( "Turret target must be an entity or a vector." );
|
|
}
|
|
}
|
|
|
|
function FireTurret( turretIdx, isFake )
|
|
{
|
|
self FireWeapon( turretIdx, undefined, undefined, self );
|
|
}
|
|
|
|
function Javelin_LoseTargetAtRightTime( target )
|
|
{
|
|
self endon( "death" );
|
|
|
|
self waittill( "weapon_fired", proj );
|
|
|
|
if( !isdefined( proj ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
proj endon( "death" );
|
|
|
|
wait 2;
|
|
|
|
while( isdefined( target ) )
|
|
{
|
|
if( proj GetVelocity()[2] < -150 && DistanceSquared( proj.origin, target.origin ) < ( (1200) * (1200) ) )
|
|
{
|
|
proj Missile_SetTarget( undefined );
|
|
break;
|
|
}
|
|
wait 0.1;
|
|
}
|
|
}
|
|
|
|
function waittill_pathing_done( maxtime = 15 )
|
|
{
|
|
self endon( "change_state" );
|
|
|
|
self util::waittill_any_ex( maxtime, "near_goal", "force_goal", "reached_end_node", "goal", "pathfind_failed", "change_state" );
|
|
}
|
|
|
|
function waittill_pathresult( maxtime = 0.5 )
|
|
{
|
|
self endon( "change_state" );
|
|
|
|
result = self util::waittill_any_timeout( maxtime, "pathfind_failed", "pathfind_succeeded", "change_state" );
|
|
succeeded = ( result === "pathfind_succeeded" );
|
|
return succeeded;
|
|
}
|
|
|
|
function waittill_asm_terminated()
|
|
{
|
|
self endon( "death" );
|
|
self notify( "end_asm_terminated_thread" );
|
|
self endon( "end_asm_terminated_thread" );
|
|
self waittill( "asm_terminated" );
|
|
self notify( "asm_complete", "__terminated__" );
|
|
}
|
|
|
|
function waittill_asm_timeout( timeout )
|
|
{
|
|
self endon( "death" );
|
|
self notify( "end_asm_timeout_thread" );
|
|
self endon( "end_asm_timeout_thread" );
|
|
wait timeout;
|
|
self notify( "asm_complete", "__timeout__" );
|
|
}
|
|
|
|
function waittill_asm_complete( substate_to_wait, timeout = 10 )
|
|
{
|
|
self endon( "death" );
|
|
|
|
self thread waittill_asm_terminated();
|
|
self thread waittill_asm_timeout( timeout );
|
|
|
|
substate = undefined;
|
|
while ( !isdefined( substate ) || ( substate != substate_to_wait && substate != "__terminated__" && substate != "__timeout__" ) )
|
|
{
|
|
self waittill( "asm_complete", substate );
|
|
}
|
|
self notify( "end_asm_terminated_thread" );
|
|
self notify( "end_asm_timeout_thread" );
|
|
}
|
|
|
|
// self == vehicle
|
|
function throw_off_balance( damageType, hitPoint, hitDirection, hitLocationInfo )
|
|
{
|
|
if ( damageType == "MOD_EXPLOSIVE" || damageType == "MOD_GRENADE_SPLASH" || damageType == "MOD_PROJECTILE_SPLASH" )
|
|
{
|
|
self SetVehVelocity( self.velocity + VectorNormalize( hitDirection ) * 300 );
|
|
ang_vel = self GetAngularVelocity();
|
|
ang_vel += ( RandomFloatRange( -300, 300 ), RandomFloatRange( -300, 300 ), RandomFloatRange( -300, 300 ) );
|
|
self SetAngularVelocity( ang_vel );
|
|
}
|
|
else
|
|
{
|
|
ang_vel = self GetAngularVelocity();
|
|
yaw_vel = RandomFloatRange( -320, 320 );
|
|
yaw_vel += math::sign( yaw_vel ) * 150;
|
|
|
|
ang_vel += ( RandomFloatRange( -150, 150 ), yaw_vel, RandomFloatRange( -150, 150 ) );
|
|
self SetAngularVelocity( ang_vel );
|
|
}
|
|
}
|
|
|
|
function predicted_collision()
|
|
{
|
|
self endon( "crash_done" );
|
|
self endon( "death" );
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "veh_predictedcollision", velocity, normal );
|
|
if ( normal[2] >= 0.6 )
|
|
{
|
|
self notify( "veh_collision", velocity, normal );
|
|
}
|
|
}
|
|
}
|
|
|
|
// self == vehicle
|
|
function collision_fx( normal )
|
|
{
|
|
tilted = ( normal[2] < 0.6 );
|
|
fx_origin = self.origin - normal * ( tilted ? 28 : 10 );
|
|
|
|
self PlaySound( "veh_wasp_wall_imp" );
|
|
//PlayFX( level._effect[ "drone_nudge" ], fx_origin, normal );
|
|
}
|
|
|
|
// self == vehicle
|
|
function nudge_collision()
|
|
{
|
|
self endon( "crash_done" );
|
|
self endon( "power_off_done" );
|
|
self endon( "death" );
|
|
self notify( "end_nudge_collision" );
|
|
self endon( "end_nudge_collision" );
|
|
|
|
if ( self.notsolid === true )
|
|
{
|
|
return;
|
|
}
|
|
|
|
while ( 1 )
|
|
{
|
|
self waittill( "veh_collision", velocity, normal );
|
|
ang_vel = self GetAngularVelocity() * 0.5;
|
|
self SetAngularVelocity( ang_vel );
|
|
|
|
empedOrOff = ( self get_current_state() === "emped" || self get_current_state() === "off" );
|
|
|
|
// bounce off walls
|
|
if ( IsAlive( self ) && ( normal[2] < 0.6 || !empedOrOff ) ) // stable or active
|
|
{
|
|
self SetVehVelocity( self.velocity + normal * 90 );
|
|
self collision_fx( normal );
|
|
}
|
|
else if ( empedOrOff )
|
|
{
|
|
if ( isdefined( self.bounced ) )
|
|
{
|
|
self playsound( "veh_wasp_wall_imp" );
|
|
self SetVehVelocity( ( 0, 0, 0 ) );
|
|
self SetAngularVelocity( ( 0, 0, 0 ) );
|
|
|
|
pitch = self.angles[0];
|
|
pitch = math::sign( pitch ) * math::clamp( abs( pitch ), 10, 15 );
|
|
self.angles = ( pitch, self.angles[1], self.angles[2] );
|
|
|
|
self.bounced = undefined;
|
|
self notify( "landed" );
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
self.bounced = true;
|
|
self SetVehVelocity( self.velocity + normal * 30 );
|
|
self collision_fx( normal );
|
|
}
|
|
}
|
|
else // tilted
|
|
{
|
|
impact_vel = abs( VectorDot( velocity, normal ) );
|
|
|
|
if( normal[2] < 0.6 && impact_vel < 100 )
|
|
{
|
|
self SetVehVelocity( self.velocity + normal * 90 );
|
|
self collision_fx( normal );
|
|
}
|
|
else
|
|
{
|
|
self playsound( "veh_wasp_ground_death" );
|
|
self thread vehicle_death::death_fire_loop_audio();
|
|
self notify( "crash_done" );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//self is vehicle
|
|
function level_out_for_landing()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self endon( "landed" );
|
|
|
|
while( 1 )
|
|
{
|
|
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 + (0,0,-60) );
|
|
{wait(.05);};
|
|
}
|
|
}
|
|
|
|
|
|
//self is vehicle
|
|
function immolate( attacker )
|
|
{
|
|
self endon( "death" );
|
|
|
|
self thread burning_thread( attacker, attacker );
|
|
}
|
|
|
|
function burning_thread( attacker, inflictor )
|
|
{
|
|
self endon( "death" );
|
|
self notify( "end_immolating_thread" );
|
|
self endon( "end_immolating_thread" );
|
|
|
|
damagePerSecond = self.settings.burn_damagepersecond;
|
|
if ( !isdefined( damagePerSecond ) || damagePerSecond <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
secondsPerOneDamage = 1.0 / Float( damagePerSecond );
|
|
|
|
if ( !isdefined( self.abnormal_status ) )
|
|
{
|
|
self.abnormal_status = spawnStruct();
|
|
}
|
|
|
|
if ( self.abnormal_status.burning !== true )
|
|
{
|
|
self vehicle::toggle_burn_fx( true );
|
|
}
|
|
|
|
self.abnormal_status.burning = true;
|
|
self.abnormal_status.attacker = attacker;
|
|
self.abnormal_status.inflictor = inflictor;
|
|
|
|
lastingTime = self.settings.burn_lastingtime;
|
|
if ( !isdefined( lastingTime ) )
|
|
{
|
|
lastingTime = 999999;
|
|
}
|
|
|
|
startTime = GetTime();
|
|
interval = max( secondsPerOneDamage, 0.5 );
|
|
damage = 0.0;
|
|
|
|
while ( TimeSince( startTime ) < lastingTime )
|
|
{
|
|
previousTime = GetTime();
|
|
wait interval;
|
|
damage += TimeSince( previousTime ) * damagePerSecond;
|
|
damageInt = Int( damage );
|
|
self DoDamage( damageInt, self.origin, attacker, self, "none", "MOD_BURNED" );
|
|
damage -= damageInt;
|
|
}
|
|
|
|
self.abnormal_status.burning = false;
|
|
self vehicle::toggle_burn_fx( false );
|
|
}
|
|
|
|
//self is vehicle
|
|
function iff_notifyMeInNSec(time,note)
|
|
{
|
|
self endon("death");
|
|
wait time;
|
|
self notify(note);
|
|
}
|
|
function iff_override( owner,time = 60 )
|
|
{
|
|
self endon( "death" );
|
|
|
|
//save off the old team
|
|
self._iffoverride_oldTeam = self.team;
|
|
|
|
self iff_override_team_switch_behavior( owner.team );
|
|
if(isDefined(self.iff_override_cb))
|
|
self [[self.iff_override_cb]](true);
|
|
|
|
if(isDefined(self.settings) && !( isdefined( self.settings.iffshouldrevertteam ) && self.settings.iffshouldrevertteam ) )
|
|
{
|
|
//do not revert team
|
|
return;
|
|
}
|
|
timeout = (isDefined(self.settings)?self.settings.ifftimetillrevert:time);
|
|
assert(timeout>10);
|
|
self thread iff_notifyMeInNSec(timeout-10,"iff_override_revert_warn");//5 defined in _cybercom.gsh;
|
|
msg = self util::waittill_any_timeout( timeout,"iff_override_reverted", "death" );
|
|
if (msg == "timeout" )
|
|
{
|
|
self notify ("iff_override_reverted");
|
|
}
|
|
|
|
self playsound ("gdt_iff_deactivate");
|
|
|
|
self iff_override_team_switch_behavior( self._iffoverride_oldTeam );
|
|
if(isDefined(self.iff_override_cb))
|
|
self [[self.iff_override_cb]](false);
|
|
|
|
}
|
|
|
|
function iff_override_team_switch_behavior( team )
|
|
{
|
|
self endon( "death" );
|
|
|
|
old_ignoreme = self.ignoreme;
|
|
self.ignoreme = true;
|
|
|
|
self start_scripted();
|
|
self vehicle::lights_off();
|
|
wait 0.1;
|
|
|
|
wait( 1 );
|
|
|
|
self SetTeam( team );
|
|
|
|
self blink_lights_for_time( 1 );
|
|
|
|
self stop_scripted();
|
|
|
|
wait 1;
|
|
self.ignoreme = old_ignoreme;
|
|
}
|
|
|
|
function blink_lights_for_time( time )
|
|
{
|
|
self endon( "death" );
|
|
|
|
startTime = GetTime();
|
|
|
|
self vehicle::lights_off();
|
|
wait 0.1;
|
|
|
|
while ( GetTime() < startTime + time * 1000 )
|
|
{
|
|
self vehicle::lights_off();
|
|
wait 0.2;
|
|
self vehicle::lights_on();
|
|
wait 0.2;
|
|
}
|
|
|
|
self vehicle::lights_on();
|
|
}
|
|
|
|
function TurnOff()
|
|
{
|
|
self notify( "shut_off" );
|
|
}
|
|
|
|
function TurnOn()
|
|
{
|
|
self notify( "start_up" );
|
|
}
|
|
|
|
function TurnOffAllLightsAndLaser()
|
|
{
|
|
self LaserOff();
|
|
self vehicle::lights_off();
|
|
self vehicle::toggle_lights_group( 1, false );
|
|
self vehicle::toggle_lights_group( 2, false );
|
|
self vehicle::toggle_lights_group( 3, false );
|
|
self vehicle::toggle_lights_group( 4, false );
|
|
self vehicle::toggle_burn_fx( false );
|
|
self vehicle::toggle_emp_fx( false );
|
|
}
|
|
|
|
function TurnOffAllAmbientAnims()
|
|
{
|
|
self vehicle::toggle_ambient_anim_group( 1, false );
|
|
self vehicle::toggle_ambient_anim_group( 2, false );
|
|
self vehicle::toggle_ambient_anim_group( 3, false );
|
|
}
|
|
|
|
function ClearAllLookingAndTargeting()
|
|
{
|
|
self ClearTargetEntity();
|
|
self ClearGunnerTarget(0);
|
|
self ClearGunnerTarget(1);
|
|
self ClearGunnerTarget(2);
|
|
self ClearGunnerTarget(3);
|
|
self ClearLookAtEnt();
|
|
}
|
|
|
|
function ClearAllMovement( zeroOutSpeed = false )
|
|
{
|
|
if( !IsAirborne( self ) )
|
|
{
|
|
self CancelAIMove();
|
|
}
|
|
self ClearVehGoalPos();
|
|
self PathVariableOffsetClear();
|
|
self PathFixedOffsetClear();
|
|
|
|
if ( zeroOutSpeed === true )
|
|
{
|
|
self notify( "landed" );
|
|
self SetVehVelocity( ( 0, 0, 0 ) );
|
|
self SetPhysAcceleration( ( 0, 0, 0 ) );
|
|
self SetAngularVelocity( ( 0, 0, 0 ) );
|
|
}
|
|
}
|
|
|
|
function shared_callback_damage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, weapon, vPoint, vDir, sHitLoc, vDamageOrigin, psOffsetTime, damageFromUnderneath, modelIndex, partName, vSurfaceNormal )
|
|
{
|
|
if ( should_emp( self, weapon, sMeansOfDeath, eInflictor, eAttacker ) )
|
|
{
|
|
minEmpDownTime = 0.8 * self.settings.empdowntime;
|
|
maxEmpDownTime = 1.2 * self.settings.empdowntime;
|
|
self notify ( "emped", RandomFloatRange( minEmpDownTime, maxEmpDownTime ), eAttacker, eInflictor );
|
|
}
|
|
|
|
if ( should_burn( self, weapon, sMeansOfDeath, eInflictor, eAttacker ) )
|
|
{
|
|
self thread burning_thread( eAttacker, eInflictor );
|
|
}
|
|
|
|
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 )
|
|
{
|
|
self notify( "pain" );
|
|
}
|
|
vehicle::set_damage_fx_level( self.damageLevel );
|
|
}
|
|
|
|
return iDamage;
|
|
}
|
|
|
|
function should_emp( vehicle, weapon, meansOfDeath, eInflictor, eAttacker )
|
|
{
|
|
if( !isdefined( vehicle ) || meansOfDeath === "MOD_IMPACT" || vehicle.disableElectroDamage === true )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( !( ( isdefined( weapon ) && weapon.isEmp ) || meansOfDeath === "MOD_ELECTROCUTED" ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
causer = ( isdefined( eAttacker ) ? eAttacker : eInflictor );
|
|
if ( !isdefined( causer ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( IsAI( causer ) && IsVehicle( causer ) ) // don't make electro contagious between AI vehicles
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( level.teamBased )
|
|
{
|
|
return ( vehicle.team != causer.team );
|
|
}
|
|
else
|
|
{
|
|
if ( isdefined( vehicle.owner ) )
|
|
{
|
|
return ( vehicle.owner != causer );
|
|
}
|
|
|
|
return ( vehicle != causer );
|
|
}
|
|
}
|
|
|
|
function should_burn( vehicle, weapon, meansOfDeath, eInflictor, eAttacker )
|
|
{
|
|
if ( level.disableVehicleBurnDamage === true || vehicle.disableBurnDamage === true )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if( !isdefined( vehicle ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( meansOfDeath !== "MOD_BURNED" )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( vehicle === eInflictor ) // this is how we do continuesly burning damage
|
|
{
|
|
return false;
|
|
}
|
|
|
|
causer = ( isdefined( eAttacker ) ? eAttacker : eInflictor );
|
|
if ( !isdefined( causer ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( IsAI( causer ) && IsVehicle( causer ) ) // don't make burning contagious between AI vehicles
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( level.teamBased )
|
|
{
|
|
return ( vehicle.team != causer.team );
|
|
}
|
|
else
|
|
{
|
|
if ( isdefined( vehicle.owner ) )
|
|
{
|
|
return ( vehicle.owner != causer );
|
|
}
|
|
|
|
return ( vehicle != causer );
|
|
}
|
|
}
|
|
|
|
//*****************************************************************************
|
|
//
|
|
// Vehicle AI states
|
|
//
|
|
// - These are the default states if the vehicle type don't change the callback
|
|
// - To change a specific callback:
|
|
// self get_state_callbacks("combat").update_func = custom_update_func;
|
|
// - update_func will be called on a thread, generally have a loop inside and endon( "change_state" )
|
|
// * update_func can have wait, or set state in it
|
|
// - enter_func and exit_func are guaranteed to be called on state transition, so it's good for init and clean up for a state
|
|
// * enter_func and exit_func should not have wait, or set state in it
|
|
//
|
|
//*****************************************************************************
|
|
|
|
// self == vehicle
|
|
function StartInitialState( defaultState = "combat" )
|
|
{
|
|
// we expect script_startstate to be only changed from Radiant, not in script, so we don't wait here
|
|
|
|
params = SpawnStruct();
|
|
params.isInitialState = true;
|
|
|
|
// Set the first state
|
|
if ( isdefined( self.script_startstate ) )
|
|
{
|
|
self set_state( self.script_startstate, params );
|
|
}
|
|
else
|
|
{
|
|
// Set the first state
|
|
self set_state( defaultState, params );
|
|
}
|
|
}
|
|
|
|
// self == vehicle
|
|
function start_scripted( disable_death_state, no_clear_movement )
|
|
{
|
|
params = spawnStruct();
|
|
params.no_clear_movement = no_clear_movement;
|
|
self set_state( "scripted", params );
|
|
self._no_death_state = disable_death_state;
|
|
}
|
|
|
|
function stop_scripted( statename )
|
|
{
|
|
if ( isAlive( self ) && is_instate( "scripted" ) )
|
|
{
|
|
if ( isdefined( statename ) )
|
|
{
|
|
self set_state( statename );
|
|
}
|
|
else
|
|
{
|
|
self set_state( "combat" );
|
|
}
|
|
}
|
|
}
|
|
|
|
// self == vehicle
|
|
function set_role( rolename )
|
|
{
|
|
self.current_role = rolename;
|
|
}
|
|
|
|
// self == vehicle
|
|
function set_state( name, params )
|
|
{
|
|
self.state_machines[ self.current_role ] thread statemachine::set_state( name, params );
|
|
}
|
|
|
|
function evaluate_connections( eval_func, params )
|
|
{
|
|
self.state_machines[ self.current_role ] statemachine::evaluate_connections( eval_func, params );
|
|
}
|
|
|
|
// self == vehicle
|
|
function get_state_callbacks( statename )
|
|
{
|
|
rolename = "default";
|
|
if ( isdefined( self.current_role ) )
|
|
{
|
|
rolename = self.current_role;
|
|
}
|
|
|
|
if ( IsDefined( self.state_machines[ rolename ] ) )
|
|
{
|
|
return self.state_machines[ rolename ].states[ statename ];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// self == vehicle
|
|
function get_state_callbacks_for_role( rolename, statename )
|
|
{
|
|
if ( !isdefined( rolename ) )
|
|
{
|
|
rolename = "default";
|
|
}
|
|
|
|
if ( IsDefined( self.state_machines[ rolename ] ) )
|
|
{
|
|
return self.state_machines[ rolename ].states[ statename ];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// self == vehicle
|
|
function get_current_state()
|
|
{
|
|
if ( IsDefined( self.current_role ) && IsDefined( self.state_machines[ self.current_role ].current_state ) )
|
|
{
|
|
return self.state_machines[ self.current_role ].current_state.name;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// self == vehicle
|
|
function get_previous_state()
|
|
{
|
|
if ( IsDefined( self.current_role ) && IsDefined( self.state_machines[ self.current_role ].previous_state ) )
|
|
{
|
|
return self.state_machines[ self.current_role ].previous_state.name;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// self == vehicle
|
|
function get_next_state()
|
|
{
|
|
if ( IsDefined( self.current_role ) && IsDefined( self.state_machines[ self.current_role ].next_state ) )
|
|
{
|
|
return self.state_machines[ self.current_role ].next_state.name;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
|
|
// self == vehicle
|
|
function is_instate( statename )
|
|
{
|
|
if ( IsDefined( self.current_role ) && IsDefined( self.state_machines[ self.current_role ].current_state ) )
|
|
{
|
|
return self.state_machines[ self.current_role ].current_state.name === statename;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// self == vehicle
|
|
function add_state( name, enter_func, update_func, exit_func )
|
|
{
|
|
if ( IsDefined( self.current_role ) )
|
|
{
|
|
statemachine = self.state_machines[ self.current_role ];
|
|
|
|
if ( IsDefined( statemachine ) )
|
|
{
|
|
state = statemachine statemachine::add_state( name, enter_func, update_func, exit_func );
|
|
return state;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// self == vehicle
|
|
function add_interrupt_connection( from_state_name, to_state_name, on_notify, checkfunc )
|
|
{
|
|
self.state_machines[ self.current_role ] statemachine::add_interrupt_connection( from_state_name, to_state_name, on_notify, checkfunc );
|
|
}
|
|
|
|
// self == vehicle
|
|
function add_utility_connection( from_state_name, to_state_name, checkfunc, defaultScore )
|
|
{
|
|
self.state_machines[ self.current_role ] statemachine::add_utility_connection( from_state_name, to_state_name, checkfunc, defaultScore );
|
|
}
|
|
|
|
// self == vehicle
|
|
function init_state_machine_for_role( rolename )
|
|
{
|
|
if ( !isdefined( rolename ) )
|
|
{
|
|
rolename = "default";
|
|
}
|
|
|
|
statemachine = statemachine::create( rolename, self );
|
|
statemachine.isRole = true;
|
|
|
|
if ( !IsDefined( self.current_role ) )
|
|
{
|
|
set_role( rolename );
|
|
}
|
|
|
|
// special states
|
|
statemachine statemachine::add_state( "suspend", // empty state used to "suspend" the state machine, not to be confused with "off" state
|
|
undefined,
|
|
undefined,
|
|
undefined );
|
|
|
|
statemachine statemachine::add_state( "death",
|
|
&defaultstate_death_enter,
|
|
&defaultstate_death_update,
|
|
undefined );
|
|
|
|
statemachine statemachine::add_state( "scripted",
|
|
&defaultstate_scripted_enter,
|
|
undefined, // no update (scripter taking manual control or player controlling)
|
|
&defaultstate_scripted_exit );
|
|
|
|
// general states
|
|
statemachine statemachine::add_state( "combat",
|
|
&defaultstate_combat_enter,
|
|
undefined,
|
|
&defaultstate_combat_exit );
|
|
|
|
statemachine statemachine::add_state( "emped",
|
|
&defaultstate_emped_enter,
|
|
&defaultstate_emped_update,
|
|
&defaultstate_emped_exit,
|
|
&defaultstate_emped_reenter );
|
|
|
|
statemachine statemachine::add_state( "surge",
|
|
&defaultstate_surge_enter,
|
|
&defaultstate_surge_update,
|
|
&defaultstate_surge_exit );
|
|
|
|
statemachine statemachine::add_state( "off",
|
|
&defaultstate_off_enter,
|
|
undefined,
|
|
&defaultstate_off_exit );
|
|
|
|
statemachine statemachine::add_state( "driving",
|
|
&defaultstate_driving_enter,
|
|
undefined,
|
|
&defaultstate_driving_exit );
|
|
|
|
statemachine statemachine::add_state( "pain",
|
|
&defaultstate_pain_enter,
|
|
undefined,
|
|
&defaultstate_pain_exit );
|
|
|
|
|
|
statemachine statemachine::add_interrupt_connection( "off", "combat", "start_up" );
|
|
statemachine statemachine::add_interrupt_connection( "driving", "combat", "exit_vehicle" );
|
|
statemachine statemachine::add_utility_connection( "emped", "combat" );
|
|
statemachine statemachine::add_utility_connection( "pain", "combat" );
|
|
|
|
statemachine statemachine::add_interrupt_connection( "combat", "emped", "emped" );
|
|
statemachine statemachine::add_interrupt_connection( "pain", "emped", "emped" );
|
|
statemachine statemachine::add_interrupt_connection( "emped", "emped", "emped" );// emped vehicle can be emped again, it will delay the recover
|
|
|
|
statemachine statemachine::add_interrupt_connection( "combat", "surge", "surge" );
|
|
statemachine statemachine::add_interrupt_connection( "off", "surge", "surge" );
|
|
statemachine statemachine::add_interrupt_connection( "pain", "surge", "surge" );
|
|
statemachine statemachine::add_interrupt_connection( "emped", "surge", "surge" );
|
|
|
|
statemachine statemachine::add_interrupt_connection( "combat", "off", "shut_off" );
|
|
statemachine statemachine::add_interrupt_connection( "emped", "off", "shut_off" );
|
|
statemachine statemachine::add_interrupt_connection( "pain", "off", "shut_off" );
|
|
// no "driving" to "off". the player will need to be kicked out first.
|
|
|
|
statemachine statemachine::add_interrupt_connection( "combat", "driving", "enter_vehicle" );
|
|
statemachine statemachine::add_interrupt_connection( "emped", "driving", "enter_vehicle" );
|
|
statemachine statemachine::add_interrupt_connection( "off", "driving", "enter_vehicle" );
|
|
statemachine statemachine::add_interrupt_connection( "pain", "driving", "enter_vehicle" );
|
|
|
|
statemachine statemachine::add_interrupt_connection( "combat", "pain", "pain" );
|
|
statemachine statemachine::add_interrupt_connection( "emped", "pain", "pain" );
|
|
statemachine statemachine::add_interrupt_connection( "off", "pain", "pain" );
|
|
statemachine statemachine::add_interrupt_connection( "driving", "pain", "pain" );
|
|
|
|
// no connection to "death". "death" state is handled in this callback as a special case
|
|
self.overrideVehicleKilled = &Callback_VehicleKilled;
|
|
self.overrideVehicleDeathPostGame = &Callback_VehicleKilled;
|
|
|
|
// no connection to "scripted". start_scripted and stop_scripted are used to control the transition in and out of "scripted" state.
|
|
|
|
statemachine thread statemachine::set_state("suspend");
|
|
|
|
self thread on_death_cleanup();
|
|
|
|
return statemachine;
|
|
}
|
|
|
|
//if levels or gamemodes want to add custom vehicle states, they can register them to this list
|
|
|
|
function register_custom_add_state_callback( func )
|
|
{
|
|
if( !IsDefined( level.level_specific_add_state_callbacks ) )
|
|
{
|
|
level.level_specific_add_state_callbacks = [];
|
|
}
|
|
|
|
level.level_specific_add_state_callbacks[level.level_specific_add_state_callbacks.size] = func;
|
|
}
|
|
|
|
function call_custom_add_state_callbacks()
|
|
{
|
|
if( IsDefined( level.level_specific_add_state_callbacks ) )
|
|
{
|
|
for( i = 0; i < level.level_specific_add_state_callbacks.size; i++ )
|
|
{
|
|
self [[ level.level_specific_add_state_callbacks[i] ]]();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: death
|
|
// ----------------------------------------------
|
|
// self == vehicle
|
|
function Callback_VehicleKilled( eInflictor, eAttacker, iDamage, sMeansOfDeath, weapon, vDir, sHitLoc, psOffsetTime )
|
|
{
|
|
if ( ( isdefined( self._no_death_state ) && self._no_death_state ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
death_info = SpawnStruct();
|
|
death_info.inflictor = eInflictor;
|
|
death_info.attacker = eAttacker;
|
|
death_info.damage = iDamage;
|
|
death_info.meansOfDeath = sMeansOfDeath;
|
|
death_info.weapon = weapon;
|
|
death_info.dir = vDir;
|
|
death_info.hitLoc = sHitLoc;
|
|
death_info.timeOffset = psOffsetTime;
|
|
|
|
self set_state( "death", death_info );
|
|
}
|
|
|
|
function on_death_cleanup()
|
|
{
|
|
state_machines = self.state_machines;
|
|
|
|
self waittill("free_vehicle");
|
|
|
|
//Clear State Machine or we will have a script leak caused by cross reference
|
|
foreach(stateMachine in state_machines)
|
|
{
|
|
stateMachine statemachine::clear();
|
|
}
|
|
}
|
|
|
|
function defaultstate_death_enter( params )
|
|
{
|
|
self vehicle::toggle_tread_fx( false );
|
|
self vehicle::toggle_exhaust_fx( false );
|
|
self vehicle::toggle_sounds( false );
|
|
self DisableAimAssist();
|
|
|
|
TurnOffAllLightsAndLaser();
|
|
TurnOffAllAmbientAnims();
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
self CancelAIMove();
|
|
|
|
//self SetBrake( 1 );
|
|
self.takedamage = 0;
|
|
self vehicle_death::death_cleanup_level_variables();
|
|
}
|
|
|
|
function burning_death_fx()
|
|
{
|
|
if ( isdefined( self.settings.burn_death_fx_1 ) && isdefined( self.settings.burn_death_tag_1 ) )
|
|
{
|
|
PlayFxOnTag( self.settings.burn_death_fx_1, self, self.settings.burn_death_tag_1 );
|
|
}
|
|
|
|
if ( isdefined( self.settings.burn_death_sound_1 ) )
|
|
{
|
|
self PlaySound( self.settings.burn_death_sound_1 );
|
|
}
|
|
}
|
|
|
|
function emp_death_fx()
|
|
{
|
|
if ( isdefined( self.settings.emp_death_fx_1 ) && isdefined( self.settings.emp_death_tag_1 ) )
|
|
{
|
|
PlayFxOnTag( self.settings.emp_death_fx_1, self, self.settings.emp_death_tag_1 );
|
|
}
|
|
|
|
if ( isdefined( self.settings.emp_death_sound_1 ) )
|
|
{
|
|
self PlaySound( self.settings.emp_death_sound_1 );
|
|
}
|
|
}
|
|
|
|
function death_radius_damage_special( radiusScale, meansOfDamage )
|
|
{
|
|
self endon( "death" );
|
|
|
|
if ( !isdefined( self ) || self.abandoned === true || self.damage_on_death === false || self.radiusdamageradius <= 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
position = self.origin + ( 0,0,15 );
|
|
radius = self.radiusdamageradius * radiusScale;
|
|
damageMax = self.radiusdamagemax;
|
|
damageMin = self.radiusdamagemin;
|
|
|
|
{wait(.05);};
|
|
|
|
if ( isdefined( self ) )
|
|
{
|
|
self RadiusDamage( position, radius, damageMax, damageMin, undefined, meansOfDamage );
|
|
}
|
|
}
|
|
|
|
function burning_death( params )
|
|
{
|
|
self endon( "death" );
|
|
self burning_death_fx();
|
|
self.skipFriendlyFireCheck = true; // burning explosion damage friendlies
|
|
self thread death_radius_damage_special( 2, "MOD_BURNED" );
|
|
self vehicle_death::set_death_model( self.deathmodel, self.modelswapdelay );
|
|
self vehicle::do_death_dynents( 3 );
|
|
self vehicle_death::DeleteWhenSafe( 10 );
|
|
}
|
|
|
|
function emped_death( params )
|
|
{
|
|
self endon( "death" );
|
|
self emp_death_fx();
|
|
self.skipFriendlyFireCheck = true; // emp explosion damage friendlies
|
|
self thread death_radius_damage_special( 2, "MOD_ELECTROCUTED");
|
|
self vehicle_death::set_death_model( self.deathmodel, self.modelswapdelay );
|
|
self vehicle::do_death_dynents( 2 );
|
|
self vehicle_death::DeleteWhenSafe();
|
|
}
|
|
|
|
function gibbed_death( params )
|
|
{
|
|
self endon( "death" );
|
|
self vehicle_death::death_fx();
|
|
self thread vehicle_death::death_radius_damage();
|
|
self vehicle_death::set_death_model( self.deathmodel, self.modelswapdelay );
|
|
self vehicle::do_death_dynents();
|
|
self vehicle_death::DeleteWhenSafe();
|
|
}
|
|
|
|
function default_death( params )
|
|
{
|
|
self endon( "death" );
|
|
self vehicle_death::death_fx();
|
|
self thread vehicle_death::death_radius_damage();
|
|
self vehicle_death::set_death_model( self.deathmodel, self.modelswapdelay );
|
|
|
|
if( isdefined( level.disable_thermal ) )
|
|
{
|
|
[[level.disable_thermal]]();
|
|
}
|
|
|
|
waittime = (isdefined(self.waittime_before_delete)?self.waittime_before_delete:0);
|
|
|
|
owner = self GetVehicleOwner();
|
|
if ( isDefined( owner ) && self isRemoteControl() )
|
|
{
|
|
// make sure player see some destruction
|
|
waittime = max( waittime, 4 );
|
|
}
|
|
|
|
util::waitForTime( waittime );
|
|
vehicle_death::FreeWhenSafe();
|
|
}
|
|
|
|
function get_death_type( params )
|
|
{
|
|
if ( self.delete_on_death === true )
|
|
{
|
|
death_type = "default";
|
|
}
|
|
else
|
|
{
|
|
death_type = self.death_type;
|
|
}
|
|
|
|
if ( !isdefined( death_type ) )
|
|
{
|
|
death_type = params.death_type;
|
|
}
|
|
|
|
// burning
|
|
if( !isdefined( death_type ) && isdefined( self.abnormal_status ) && self.abnormal_status.burning === true )
|
|
{
|
|
death_type = "burning";
|
|
}
|
|
|
|
// emped
|
|
if ( !isdefined( death_type ) && ( isdefined( self.abnormal_status ) && self.abnormal_status.emped === true ) ||
|
|
( isdefined( params.weapon ) && params.weapon.isEmp ) )
|
|
{
|
|
death_type = "emped";
|
|
}
|
|
|
|
return death_type;
|
|
}
|
|
|
|
function defaultstate_death_update( params )
|
|
{
|
|
self endon( "death" );
|
|
|
|
if( IsDefined( level.vehicle_destructer_cb ) )
|
|
{
|
|
[[level.vehicle_destructer_cb]]( self );
|
|
}
|
|
|
|
if ( self.delete_on_death === true )
|
|
{
|
|
default_death( params );
|
|
vehicle_death::DeleteWhenSafe( 0.25 );
|
|
}
|
|
else
|
|
{
|
|
death_type = (isdefined(get_death_type( params ))?get_death_type( params ):"default");
|
|
|
|
switch( death_type )
|
|
{
|
|
case "burning": burning_death( params ); break;
|
|
case "emped": emped_death( params ); break;
|
|
case "gibbed": gibbed_death( params ); break;
|
|
default: default_death( params ); break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
|
|
// ----------------------------------------------
|
|
// State: scripted
|
|
// ----------------------------------------------
|
|
function defaultstate_scripted_enter( params )
|
|
{
|
|
if ( params.no_clear_movement !== true )
|
|
{
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
if ( HasASM( self ) )
|
|
{
|
|
self ASMRequestSubstate( "locomotion@movement" );
|
|
}
|
|
self ResumeSpeed();
|
|
}
|
|
}
|
|
|
|
// no defaultstate_scripted_update() function
|
|
|
|
function defaultstate_scripted_exit( params )
|
|
{
|
|
if ( params.no_clear_movement !== true )
|
|
{
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: combat
|
|
// ----------------------------------------------
|
|
function defaultstate_combat_enter( params )
|
|
{
|
|
}
|
|
|
|
function defaultstate_combat_exit( params )
|
|
{
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: emped
|
|
// ----------------------------------------------
|
|
function defaultstate_emped_enter( params )
|
|
{
|
|
self vehicle::toggle_tread_fx( false );
|
|
self vehicle::toggle_exhaust_fx( false );
|
|
self vehicle::toggle_sounds( false );
|
|
|
|
params.laserOn = IsLaserOn( self );
|
|
|
|
self LaserOff();
|
|
self vehicle::lights_off();
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
|
|
if( IsAirborne( self ) )
|
|
{
|
|
self SetRotorSpeed( 0 );
|
|
}
|
|
|
|
if ( !isdefined( self.abnormal_status ) )
|
|
{
|
|
self.abnormal_status = spawnStruct();
|
|
}
|
|
|
|
self.abnormal_status.emped = true;
|
|
self.abnormal_status.attacker = params.notify_param[1];
|
|
self.abnormal_status.inflictor = params.notify_param[2];
|
|
|
|
self vehicle::toggle_emp_fx( true );
|
|
}
|
|
|
|
function emp_startup_fx()
|
|
{
|
|
if ( isdefined( self.settings.emp_startup_fx_1 ) && isdefined( self.settings.emp_startup_tag_1 ) )
|
|
{
|
|
PlayFxOnTag( self.settings.emp_startup_fx_1, self, self.settings.emp_startup_tag_1 );
|
|
}
|
|
}
|
|
|
|
function defaultstate_emped_update( params )
|
|
{
|
|
self endon ("death");
|
|
self endon ("change_state");
|
|
|
|
time = params.notify_param[0];
|
|
assert( isdefined( time ) );
|
|
Cooldown( "emped_timer", time );
|
|
|
|
while( !IsCooldownReady( "emped_timer" ) )
|
|
{
|
|
timeLeft = max( GetCooldownLeft( "emped_timer" ), 0.5 );
|
|
wait timeLeft;
|
|
}
|
|
|
|
self.abnormal_status.emped = false;
|
|
self vehicle::toggle_emp_fx( false );
|
|
self emp_startup_fx();
|
|
wait 1;
|
|
|
|
self evaluate_connections();
|
|
}
|
|
|
|
function defaultstate_emped_exit( params )
|
|
{
|
|
self vehicle::toggle_tread_fx( true );
|
|
self vehicle::toggle_exhaust_fx( true );
|
|
self vehicle::toggle_sounds( true );
|
|
|
|
if ( params.laserOn === true )
|
|
{
|
|
self LaserOn();
|
|
}
|
|
self vehicle::lights_on();
|
|
if( IsAirborne( self ) )
|
|
{
|
|
self SetPhysAcceleration( ( 0, 0, 0 ) );
|
|
self thread nudge_collision();
|
|
self SetRotorSpeed( 1 );
|
|
}
|
|
}
|
|
|
|
function defaultstate_emped_reenter( params )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: surge
|
|
// ----------------------------------------------
|
|
|
|
function defaultstate_surge_enter( params )
|
|
{
|
|
}
|
|
|
|
function defaultstate_surge_exit( params )
|
|
{
|
|
}
|
|
|
|
function defaultstate_surge_update( params )
|
|
{
|
|
self endon( "change_state" );
|
|
self endon( "death" );
|
|
|
|
if ( !isdefined( self.abnormal_status ) )
|
|
{
|
|
self.abnormal_status = spawnStruct();
|
|
}
|
|
|
|
self.abnormal_status.emped = true;
|
|
//self.abnormal_status.attacker = params.notify_param[1];
|
|
//self.abnormal_status.inflictor = params.notify_param[2];
|
|
|
|
pathfailcount = 0;
|
|
//blink the lights to make the vehicle look crazy
|
|
self thread flash_team_switching_lights();
|
|
|
|
targets = GetAITeamArray( "axis", "team3" );
|
|
ArrayRemoveValue( targets, self );
|
|
closest = ArrayGetClosest( self.origin, targets );
|
|
|
|
self SetSpeed( self.settings.surgespeedmultiplier * self.settings.defaultMoveSpeed );
|
|
|
|
startTime = GetTime();
|
|
self thread swap_team_after_time( params.notify_param[0] );
|
|
while( GetTime() - startTime < self.settings.surgetimetolive * 1000 )
|
|
{
|
|
if ( !IsDefined( closest ) )
|
|
{
|
|
self detonate( params.notify_param[0] );
|
|
}
|
|
else
|
|
{
|
|
foundpath = false;
|
|
targetPos = closest.origin + ( 0, 0, 32 );
|
|
|
|
if ( IsDefined( targetPos ) )
|
|
{
|
|
queryResult = PositionQuery_Source_Navigation( targetPos, 0, 64, 35, 5, self );
|
|
|
|
foreach ( point in queryResult.data )
|
|
{
|
|
self.current_pathto_pos = point.origin;
|
|
|
|
foundpath = self SetVehGoalPos( self.current_pathto_pos, false, true );
|
|
if ( foundpath )
|
|
{
|
|
self thread path_update_interrupt( closest, params.notify_param[0] );
|
|
|
|
pathfailcount = 0;
|
|
|
|
self vehicle_ai::waittill_pathing_done( self.settings.surgetimetolive );
|
|
|
|
try_detonate( closest, params.notify_param[0] );
|
|
|
|
break;
|
|
}
|
|
waittillframeend;
|
|
}
|
|
}
|
|
|
|
if ( !foundpath )
|
|
{
|
|
pathfailcount++;
|
|
if ( pathfailcount > 10 )
|
|
{
|
|
self detonate( params.notify_param[0] );
|
|
}
|
|
}
|
|
wait 0.2;
|
|
}
|
|
}
|
|
if( IsAlive( self ) )
|
|
{
|
|
self detonate( params.notify_param[0] );
|
|
}
|
|
}
|
|
|
|
function path_update_interrupt( closest, attacker )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
self endon( "near_goal" );
|
|
self endon( "reached_end_node" );
|
|
|
|
wait .1; // sometimes endons may get fired off so wait a bit for the goal to get updated
|
|
|
|
while( !self try_detonate( closest, attacker ) )
|
|
{
|
|
if( isdefined( self.current_pathto_pos ) )
|
|
{
|
|
if( distance2dSquared( self.current_pathto_pos, self.goalpos ) > ( (self.goalradius) * (self.goalradius) ) )
|
|
{
|
|
wait 0.5;
|
|
|
|
self notify( "near_goal" );
|
|
}
|
|
}
|
|
wait 0.1;
|
|
}
|
|
}
|
|
|
|
function swap_team_after_time( attacker )
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
|
|
wait( 0.25 * self.settings.surgetimetolive );
|
|
self SetTeam( attacker.team );
|
|
}
|
|
|
|
function try_detonate( closest, attacker )
|
|
{
|
|
if ( IsDefined( closest ) && IsAlive( closest ) )
|
|
{
|
|
if( distanceSquared( closest.origin, self.origin ) < ( (80) * (80) ) )
|
|
{
|
|
self detonate( attacker );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function detonate( attacker )
|
|
{
|
|
self SetTeam( attacker.team );
|
|
self RadiusDamage( self.origin + ( 0, 0, 5 ), self.settings.surgedamageradius, 1500, 1000, attacker, "MOD_EXPLOSIVE" );
|
|
if( IsAlive( self ) )
|
|
{
|
|
self kill();
|
|
}
|
|
}
|
|
|
|
function flash_team_switching_lights()
|
|
{
|
|
self endon( "death" );
|
|
self endon( "change_state" );
|
|
|
|
while( 1 )
|
|
{
|
|
self vehicle::lights_off();
|
|
wait( 0.1 );
|
|
self vehicle::lights_on( "allies" );
|
|
wait( 0.1 );
|
|
self vehicle::lights_off();
|
|
wait( 0.1 );
|
|
self vehicle::lights_on( "axis" );
|
|
wait( 0.1 );
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: off
|
|
// ----------------------------------------------
|
|
function defaultstate_off_enter( params )
|
|
{
|
|
self vehicle::toggle_tread_fx( false );
|
|
self vehicle::toggle_exhaust_fx( false );
|
|
self vehicle::toggle_sounds( false );
|
|
self DisableAimAssist();
|
|
|
|
params.laserOn = IsLaserOn( self );
|
|
|
|
TurnOffAllLightsAndLaser();
|
|
TurnOffAllAmbientAnims();
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
|
|
if( isdefined( level.disable_thermal ) )
|
|
{
|
|
[[level.disable_thermal]]();
|
|
}
|
|
|
|
if( IsAirborne( self ) )
|
|
{
|
|
if ( params.isInitialState !== true && params.no_falling !== true )
|
|
{
|
|
self SetPhysAcceleration( ( 0, 0, -300 ) );
|
|
self thread level_out_for_landing();
|
|
}
|
|
self SetRotorSpeed( 0 );
|
|
}
|
|
}
|
|
|
|
function defaultstate_off_exit( params )
|
|
{
|
|
self vehicle::toggle_tread_fx( true );
|
|
self vehicle::toggle_exhaust_fx( true );
|
|
self vehicle::toggle_sounds( true );
|
|
self EnableAimAssist();
|
|
if( IsAirborne( self ) )
|
|
{
|
|
self SetPhysAcceleration( ( 0, 0, 0 ) );
|
|
self thread nudge_collision();
|
|
self SetRotorSpeed( 1 );
|
|
}
|
|
if ( params.laserOn === true )
|
|
{
|
|
self LaserOn();
|
|
}
|
|
|
|
if( isdefined( level.enable_thermal ) )
|
|
{
|
|
if( self get_next_state() !== "death" )
|
|
{
|
|
[[level.enable_thermal]]();
|
|
}
|
|
}
|
|
|
|
self vehicle::lights_on();
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: driving
|
|
// ----------------------------------------------
|
|
function defaultstate_driving_enter( params )
|
|
{
|
|
params.driver = self GetSeatOccupant( 0 );
|
|
assert ( isdefined(params.driver) );
|
|
|
|
self DisableAimAssist();
|
|
|
|
if ( level.playersDrivingVehiclesBecomeInvulnerable )
|
|
{
|
|
params.driver EnableInvulnerability();
|
|
params.driver.ignoreme = true;
|
|
}
|
|
|
|
self.turretRotScale = 1;
|
|
self.team = params.driver.team;
|
|
if ( HasASM( self ) )
|
|
{
|
|
self ASMRequestSubstate( "locomotion@movement" );
|
|
}
|
|
|
|
self SetHeliHeightCap( true );
|
|
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
self CancelAIMove();
|
|
|
|
if( isdefined( params.driver ) && !isdefined( self.customDamageMonitor ) )
|
|
{
|
|
self thread vehicle::monitor_damage_as_occupant( params.driver );
|
|
}
|
|
}
|
|
|
|
function defaultstate_driving_exit( params )
|
|
{
|
|
self EnableAimAssist();
|
|
if( isdefined( params.driver ) )
|
|
{
|
|
params.driver DisableInvulnerability();
|
|
params.driver.ignoreme = false;
|
|
}
|
|
self.turretRotScale = 1;
|
|
|
|
self SetHeliHeightCap( false );
|
|
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
|
|
if( isdefined( params.driver ) )
|
|
{
|
|
params.driver vehicle::stop_monitor_damage_as_occupant();
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// State: pain
|
|
// ----------------------------------------------
|
|
function defaultstate_pain_enter( params )
|
|
{
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
}
|
|
|
|
function defaultstate_pain_exit( params )
|
|
{
|
|
ClearAllLookingAndTargeting();
|
|
ClearAllMovement();
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// Vehicle AI position finding
|
|
|
|
|
|
|
|
function CanSeeEnemyFromPosition( position, enemy, sight_check_height )
|
|
{
|
|
sightCheckOrigin = position + (0,0,sight_check_height);
|
|
return sighttracepassed( sightCheckOrigin, enemy.origin + (0,0,30), false, self );
|
|
}
|
|
|
|
function FindNewPosition( sight_check_height )
|
|
{
|
|
if( self.goalforced )
|
|
{
|
|
goalpos = GetClosestPointOnNavMesh( self.goalpos, self.radius * 2, self.radius );
|
|
///#sphere(self.goalpos,15,(0,1,0),1,true,10,100000 ) ;#/
|
|
return goalpos;
|
|
}
|
|
|
|
point_spacing = 90;
|
|
|
|
PixBeginEvent( "vehicle_ai_shared::FindNewPosition" );
|
|
queryResult = PositionQuery_Source_Navigation( self.origin, 0, 2000, 300/*half height*/, point_spacing, self, point_spacing * 2 );
|
|
PixEndEvent();
|
|
|
|
// filter
|
|
PositionQuery_filter_Random( queryResult, 0, 50 );
|
|
PositionQuery_Filter_DistanceToGoal( queryResult, self );
|
|
vehicle_ai::PositionQuery_Filter_OutOfGoalAnchor( queryResult, 50 );
|
|
|
|
origin = self.goalpos;
|
|
|
|
best_point = undefined;
|
|
best_score = -999999;
|
|
|
|
if ( isdefined( self.enemy ) )
|
|
{
|
|
PositionQuery_Filter_Sight( queryResult, self.enemy.origin, self GetEye() - self.origin, self, 0, self.enemy );
|
|
self vehicle_ai::PositionQuery_Filter_EngagementDist( queryResult, self.enemy, self.settings.engagementDistMin, self.settings.engagementDistMax );
|
|
|
|
if( turret::has_turret( 1 ) )
|
|
{
|
|
side_turret_enemy = turret::get_target( 1 );
|
|
if( isdefined( side_turret_enemy ) && side_turret_enemy != self.enemy )
|
|
{
|
|
PositionQuery_Filter_Sight( queryResult, side_turret_enemy.origin, (0,0,sight_check_height), self, 20, self, "sight2" );
|
|
}
|
|
}
|
|
|
|
if( turret::has_turret( 2 ) )
|
|
{
|
|
side_turret_enemy = turret::get_target( 2 );
|
|
if( isdefined( side_turret_enemy ) && side_turret_enemy != self.enemy )
|
|
{
|
|
PositionQuery_Filter_Sight( queryResult, side_turret_enemy.origin, (0,0,sight_check_height), self, 20, self, "sight3" );
|
|
}
|
|
}
|
|
|
|
foreach ( point in queryResult.data )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "engagementDist" ] = -point.distAwayFromEngagementArea; #/ point.score += -point.distAwayFromEngagementArea;;
|
|
|
|
if( distance2dSquared( self.origin, point.origin ) < 170 * 170 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "tooCloseToSelf" ] = -170; #/ point.score += -170;;
|
|
}
|
|
|
|
if( isdefined( point.sight ) && point.sight )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "sight" ] = 250; #/ point.score += 250;;
|
|
}
|
|
if( isdefined( point.sight2 ) && point.sight2 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "sight2" ] = 150; #/ point.score += 150;;
|
|
}
|
|
if( isdefined( point.sight3 ) && point.sight3 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "sight3" ] = 150; #/ point.score += 150;;
|
|
}
|
|
|
|
if ( point.score > best_score )
|
|
{
|
|
best_score = point.score;
|
|
best_point = point;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach ( point in queryResult.data )
|
|
{
|
|
if( distance2dSquared( self.origin, point.origin ) < 170 * 170 )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "tooCloseToSelf" ] = -100; #/ point.score += -100;;
|
|
}
|
|
|
|
if( point.score > best_score )
|
|
{
|
|
best_score = point.score;
|
|
best_point = point;
|
|
}
|
|
}
|
|
}
|
|
|
|
self vehicle_ai::PositionQuery_DebugScores( queryResult );
|
|
|
|
if( isdefined( best_point ) )
|
|
{
|
|
/#
|
|
//line( best_point, best_point + (0,0,100), (.3,1,.3), 1.0, true, 100 );
|
|
#/
|
|
origin = best_point.origin;
|
|
}
|
|
|
|
return origin + (0,0,10);
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// time handling
|
|
// ----------------------------------------------
|
|
|
|
// in seconds
|
|
// usage: attackStart = GetTime(); if( TimeSince( attackStart ) > 5 ) ...
|
|
function TimeSince( startTimeInMilliseconds )
|
|
{
|
|
return ( GetTime() - startTimeInMilliseconds ) * 0.001;
|
|
}
|
|
|
|
function CooldownInit()
|
|
{
|
|
if( !isdefined( self._cooldown ) )
|
|
{
|
|
self._cooldown = [];
|
|
}
|
|
}
|
|
|
|
function Cooldown( name, time_seconds )
|
|
{
|
|
CooldownInit();
|
|
|
|
self._cooldown[ name ] = GetTime() + time_seconds * 1000;
|
|
}
|
|
|
|
function GetCooldownTimeRaw( name )
|
|
{
|
|
CooldownInit();
|
|
|
|
if ( !isdefined( self._cooldown[ name ] ) )
|
|
{
|
|
self._cooldown[ name ] = GetTime() - 1;
|
|
}
|
|
return self._cooldown[ name ];
|
|
}
|
|
|
|
function GetCooldownLeft( name )
|
|
{
|
|
CooldownInit();
|
|
|
|
return ( GetCooldownTimeRaw( name ) - GetTime() ) * 0.001;
|
|
}
|
|
|
|
function IsCooldownReady( name, timeForward_seconds )
|
|
{
|
|
CooldownInit();
|
|
|
|
if ( !isdefined( timeForward_seconds ) )
|
|
{
|
|
timeForward_seconds = 0;
|
|
}
|
|
|
|
cooldownReadyTime = self._cooldown[ name ];
|
|
return !isdefined( cooldownReadyTime ) || GetTime() + timeForward_seconds * 1000 > cooldownReadyTime;
|
|
}
|
|
|
|
function ClearCooldown( name )
|
|
{
|
|
CooldownInit();
|
|
|
|
self._cooldown[ name ] = GetTime() - 1;
|
|
}
|
|
|
|
function AddCooldownTime( name, time_seconds )
|
|
{
|
|
CooldownInit();
|
|
|
|
self._cooldown[ name ] = GetCooldownTimeRaw( name ) + time_seconds * 1000;
|
|
}
|
|
|
|
function ClearAllCooldowns()
|
|
{
|
|
if( isdefined( self._cooldown ) )
|
|
{
|
|
foreach ( str_name, cooldown in self._cooldown )
|
|
{
|
|
self._cooldown[ str_name ] = GetTime() - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
// debug helpers for position query
|
|
// ----------------------------------------------
|
|
|
|
function PositionQuery_DebugScores( queryResult )
|
|
{
|
|
if ( !( isdefined( GetDvarInt("hkai_debugPositionQuery") ) && GetDvarInt("hkai_debugPositionQuery") ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach( point in queryResult.data )
|
|
{
|
|
point DebugScore( self );
|
|
}
|
|
}
|
|
|
|
// self == pointStruct
|
|
function DebugScore( entity )
|
|
{
|
|
/#
|
|
if ( !isdefined( self._scoreDebug ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( !( isdefined( GetDvarInt("hkai_debugPositionQuery") ) && GetDvarInt("hkai_debugPositionQuery") ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
step = 10;
|
|
count = 1;
|
|
|
|
color = (1,0,0);
|
|
if ( self.score >= 0 )
|
|
{
|
|
color = (0,1,0);
|
|
}
|
|
|
|
RecordStar( self.origin, color );
|
|
record3DText( "" + self.score + ":", self.origin - (0,0,step * count), color );
|
|
foreach( name, score in self._scoreDebug )
|
|
{
|
|
count++;
|
|
record3DText( name + " " + score, self.origin - (0,0,step * count), color );
|
|
}
|
|
#/
|
|
}
|
|
|
|
|
|
function _less_than_val( left, right )
|
|
{
|
|
if ( !isdefined( left ) )
|
|
{
|
|
return false;
|
|
}
|
|
else if ( !isdefined( right ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return left < right;
|
|
}
|
|
|
|
function _cmp_val( left, right, descending )
|
|
{
|
|
if ( descending )
|
|
{
|
|
return _less_than_val( right, left );
|
|
}
|
|
else
|
|
{
|
|
return _less_than_val( left, right );
|
|
}
|
|
}
|
|
|
|
function _sort_by_score( left, right, descending )
|
|
{
|
|
return _cmp_val( left.score, right.score, descending );
|
|
}
|
|
|
|
function PositionQuery_Filter_Random( queryResult, min, max )
|
|
{
|
|
foreach( point in queryResult.data )
|
|
{
|
|
score = RandomFloatRange( min, max );
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "random" ] = score; #/ point.score += score;;
|
|
}
|
|
}
|
|
|
|
function PositionQuery_PostProcess_SortScore( queryResult, descending = true )
|
|
{
|
|
sorted = array::merge_sort( queryResult.data, &_sort_by_score, descending );
|
|
|
|
queryResult.data = sorted;
|
|
}
|
|
|
|
function PositionQuery_Filter_OutOfGoalAnchor( queryResult, tolerance = 1 )
|
|
{
|
|
foreach( point in queryResult.data )
|
|
{
|
|
if ( point.distToGoal > tolerance )
|
|
{
|
|
score = -10000 - point.distToGoal * 10;
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "outOfGoalAnchor" ] = score; #/ point.score += score;;
|
|
}
|
|
}
|
|
}
|
|
|
|
function PositionQuery_Filter_EngagementDist( queryResult, enemy, engagementDistanceMin, engagementDistanceMax )
|
|
{
|
|
if( !isdefined( enemy ) )
|
|
return;
|
|
|
|
engagementDistance = ( engagementDistanceMin + engagementDistanceMax ) * 0.5;
|
|
half_engagement_width = Abs( engagementDistanceMax - engagementDistance );
|
|
|
|
enemy_origin = ( enemy.origin[0], enemy.origin[1], 0 );
|
|
|
|
vec_enemy_to_self = VectorNormalize( ( self.origin[0], self.origin[1], 0 ) - enemy_origin );
|
|
|
|
foreach( point in queryResult.data )
|
|
{
|
|
point.distAwayFromEngagementArea = 0; // <------- result value initialization
|
|
|
|
vec_enemy_to_point = ( point.origin[0], point.origin[1], 0 ) - enemy_origin;
|
|
|
|
|
|
dist_in_front_of_enemy = VectorDot( vec_enemy_to_point, vec_enemy_to_self );
|
|
|
|
if( abs(dist_in_front_of_enemy) < engagementDistanceMin )
|
|
{
|
|
dist_in_front_of_enemy = -engagementDistanceMin;
|
|
}
|
|
|
|
dist_away_from_sweet_line = Abs( dist_in_front_of_enemy - engagementDistance );
|
|
|
|
if( dist_away_from_sweet_line > half_engagement_width )
|
|
{
|
|
point.distAwayFromEngagementArea = dist_away_from_sweet_line - half_engagement_width;
|
|
}
|
|
|
|
too_far_dist = engagementDistanceMax * 1.1;
|
|
too_far_dist_sq = ( (too_far_dist) * (too_far_dist) );
|
|
|
|
dist_from_enemy_sq = distance2dSquared( point.origin, enemy_origin );
|
|
|
|
if( dist_from_enemy_sq > too_far_dist_sq )
|
|
{
|
|
ratioSq = dist_from_enemy_sq / too_far_dist_sq;
|
|
dist = ratioSq * too_far_dist;
|
|
dist_outside = dist - too_far_dist;
|
|
|
|
if( dist_outside > point.distAwayFromEngagementArea )
|
|
{
|
|
point.distAwayFromEngagementArea = dist_outside;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function PositionQuery_Filter_DistAwayFromTarget( queryResult, targetArray, distance, tooClosePenalty )
|
|
{
|
|
if ( !isdefined( targetArray ) || !isArray( targetArray ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach( point in queryResult.data )
|
|
{
|
|
tooClose = false;
|
|
foreach( target in targetArray )
|
|
{
|
|
origin = undefined;
|
|
if ( IsVec( target ) )
|
|
{
|
|
origin = target;
|
|
}
|
|
else if ( IsSentient( target ) && IsAlive( target ) )
|
|
{
|
|
origin = target.origin;
|
|
}
|
|
else if ( IsEntity( target ) )
|
|
{
|
|
origin = target.origin;
|
|
}
|
|
|
|
if ( isdefined( origin ) && distance2dSquared( point.origin, origin ) < ( (distance) * (distance) ) )
|
|
{
|
|
tooClose = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( tooClose )
|
|
{
|
|
/# if ( !isdefined( point._scoreDebug ) ) { point._scoreDebug = []; } point._scoreDebug[ "TooCloseToOthers" ] = tooClosePenalty; #/ point.score += tooClosePenalty;;
|
|
}
|
|
}
|
|
}
|
|
|
|
function DistancePointToEngagementHeight( origin, enemy, engagementHeightMin, engagementHeightMax )
|
|
{
|
|
if( !isdefined( enemy ) )
|
|
return undefined;
|
|
|
|
result = 0;
|
|
|
|
engagementHeight = 0.5 * ( self.settings.engagementHeightMin + self.settings.engagementHeightMax );
|
|
half_height = Abs( engagementHeightMax - engagementHeight );
|
|
|
|
targetHeight = enemy.origin[2] + engagementHeight;
|
|
distFromEngagementHeight = Abs( origin[2] - targetHeight );
|
|
|
|
if ( distFromEngagementHeight > half_height )
|
|
{
|
|
result = distFromEngagementHeight - half_height;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function PositionQuery_Filter_EngagementHeight( queryResult, enemy, engagementHeightMin, engagementHeightMax )
|
|
{
|
|
if( !isdefined( enemy ) )
|
|
return;
|
|
|
|
engagementHeight = 0.5 * ( engagementHeightMin + engagementHeightMax );
|
|
half_height = Abs( engagementHeightMax - engagementHeight );
|
|
|
|
foreach( point in queryResult.data )
|
|
{
|
|
point.distEngagementHeight = 0; // <------- result value initialization
|
|
|
|
targetHeight = enemy.origin[2] + engagementHeight;
|
|
distFromEngagementHeight = Abs( point.origin[2] - targetHeight );
|
|
|
|
if ( distFromEngagementHeight > half_height )
|
|
{
|
|
point.distEngagementHeight = distFromEngagementHeight - half_height;
|
|
}
|
|
}
|
|
}
|
|
|
|
function PositionQuery_PostProcess_RemoveOutOfGoalRadius( queryResult, tolerance = 1 )
|
|
{
|
|
for( i = 0; i < queryResult.data.size; i++ )
|
|
{
|
|
point = queryResult.data[i];
|
|
if ( point.distToGoal > tolerance )
|
|
{
|
|
ArrayRemoveIndex( queryResult.data, i );
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
|
|
function UpdatePersonalThreatBias_AttackerLockedOnToMe( threat_bias, bias_duration, get_perfect_info, update_last_seen ) // self == sentient
|
|
{
|
|
UpdatePersonalThreatBias_ViaClientFlags( self.locked_on, threat_bias, bias_duration, get_perfect_info, update_last_seen );
|
|
}
|
|
|
|
function UpdatePersonalThreatBias_AttackerLockingOnToMe( threat_bias, bias_duration, get_perfect_info, update_last_seen ) // self == sentient
|
|
{
|
|
UpdatePersonalThreatBias_ViaClientFlags( self.locking_on, threat_bias, bias_duration, get_perfect_info, update_last_seen );
|
|
}
|
|
|
|
function UpdatePersonalThreatBias_ViaClientFlags( client_flags, threat_bias, bias_duration, get_perfect_info = true, update_last_seen = true ) // self == sentient
|
|
{
|
|
assert( isdefined( client_flags ) );
|
|
|
|
remaining_flags_to_process = client_flags;
|
|
for ( i = 0; remaining_flags_to_process && i < level.players.size; i++ )
|
|
{
|
|
attacker = level.players[ i ];
|
|
if ( isdefined( attacker ) )
|
|
{
|
|
client_flag = ( 1 << attacker getEntityNumber() );
|
|
if ( client_flag & remaining_flags_to_process )
|
|
{
|
|
self SetPersonalThreatBias( attacker, Int( threat_bias ), bias_duration );
|
|
|
|
if ( get_perfect_info )
|
|
self GetPerfectInfo( attacker, update_last_seen );
|
|
|
|
remaining_flags_to_process &= ~client_flag;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/#
|
|
function UpdatePersonalThreatBias_Bots( threat_bias, bias_duration ) // self == sentient
|
|
{
|
|
foreach( player in level.players )
|
|
{
|
|
if (player util::is_bot())
|
|
{
|
|
self SetPersonalThreatBias( player, Int( threat_bias ), bias_duration );
|
|
}
|
|
}
|
|
}
|
|
#/
|
|
|
|
//switch target when someone starts to hijack
|
|
function target_hijackers()
|
|
{
|
|
self endon( "death" );
|
|
|
|
while( 1 )
|
|
{
|
|
self waittill( "ccom_lock_being_targeted", hijackingPlayer );
|
|
|
|
self GetPerfectInfo( hijackingPlayer, true );
|
|
|
|
if( isPlayer( hijackingPlayer ) )
|
|
{
|
|
self SetPersonalThreatBias( hijackingPlayer, 1500, 4.0 );
|
|
}
|
|
}
|
|
}
|
|
|