boiii-scripts/shared/vehicle_ai_shared.gsc
2023-04-13 17:30:38 +02:00

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 );
}
}
}