quake4-sdk/source/mpgame/ai/AI.cpp
2007-06-15 00:00:00 +00:00

5151 lines
141 KiB
C++

/*
================
AI.cpp
================
*/
#include "../../idlib/precompiled.h"
#pragma hdrstop
#include "../Game_local.h"
#include "AI.h"
#include "AI_Manager.h"
#include "AI_Util.h"
#include "../Projectile.h"
#include "../spawner.h"
#include "AI_Tactical.h"
const char* aiTalkMessageString [ ] = {
"None",
"primary",
"secondary",
"loop"
};
static const float AI_SIGHTDELAYSCALE = 5000.0f; // Full sight delay at 5 seconds or more of not seeing enemy
/*
===============================================================================
idAI
===============================================================================
*/
/*
=====================
idAI::idAI
=====================
*/
idAI::idAI ( void ) {
projectile_height_to_distance_ratio = 1.0f;
aas = NULL;
aasSensor = NULL;
aasFind = NULL;
lastHitCheckResult = false;
lastHitCheckTime = 0;
lastAttackTime = 0;
projectile = NULL;
projectileClipModel = NULL;
chatterTime = 0;
talkState = TALK_NEVER;
talkTarget = NULL;
talkMessage = TALKMSG_NONE;
talkBusyCount = 0;
enemy.ent = NULL;
enemy.lastVisibleChangeTime = 0;
enemy.lastVisibleTime = 0;
fl.neverDormant = false; // AI's can go dormant
allowEyeFocus = true;
disablePain = false;
allowJointMod = true;
focusEntity = NULL;
focusTime = 0;
alignHeadTime = 0;
forceAlignHeadTime = 0;
orientationJoint = INVALID_JOINT;
eyeVerticalOffset = 0.0f;
eyeHorizontalOffset = 0.0f;
headFocusRate = 0.0f;
eyeFocusRate = 0.0f;
focusAlignTime = 0;
focusRange = 0.0f;
focusType = AIFOCUS_NONE;
memset ( &combat.fl, 0, sizeof(combat.fl) );
combat.max_chasing_turn = 0;
combat.shotAtTime = 0;
combat.shotAtAngle = 0.0f;
combat.meleeRange = 0.0f;
combat.tacticalPainTaken = 0;
combat.tacticalFlinches = 0;
combat.investigateTime = 0;
combat.aggressiveScale = 1.0f;
passive.animFidgetPrefix.Clear ( );
passive.animIdlePrefix.Clear ( );
passive.animTalkPrefix.Clear ( );
passive.idleAnim.Clear();
passive.prefix.Clear();
passive.idleAnimChangeTime = 0;
passive.fidgetTime = 0;
passive.talkTime = 0;
memset ( &passive.fl, 0, sizeof(passive.fl) );
pain.lastTakenTime = 0;
pain.takenThisFrame = 0;
pain.loopEndTime = 0;
enemy.range = 0;
enemy.range2d = 0;
enemy.smoothedLinearVelocity.Zero ( );
enemy.smoothedPushedVelocity.Zero ( );
enemy.lastKnownPosition.Zero ( );
enemy.lastVisibleEyePosition.Zero ( );
enemy.lastVisibleChestPosition.Zero ( );
enemy.lastVisibleFromEyePosition.Zero ( );
enemy.checkTime = 0;
enemy.changeTime = 0;
enemy.lastVisibleChangeTime = 0;
enemy.lastVisibleTime = 0;
memset ( &enemy.fl, 0, sizeof(enemy.fl) );
currentFocusPos.Zero();
eyeAng.Zero();
lookAng.Zero();
destLookAng.Zero();
lookMin.Zero();
lookMax.Zero();
eyeMin.Zero();
eyeMax.Zero();
helperCurrent = NULL;
helperIdeal = NULL;
speakTime = 0;
actionAnimNum = 0;
actionSkipTime = 0;
actionTime = 0;
}
/*
=====================
idAI::~idAI
=====================
*/
idAI::~idAI() {
// Make sure we arent stuck in the simple think list
simpleThinkNode.Remove ( );
delete aasFind;
delete aasSensor;
delete projectileClipModel;
DeconstructScriptObject();
scriptObject.Free();
aiManager.RemoveTeammate ( this );
SetPhysics( NULL );
}
/*
=====================
idAI::Save
=====================
*/
void idAI::Save( idSaveGame *savefile ) const {
int i;
// cnicholson: These 3 vars are intentionally not saved, as noted in the restore
// NOSAVE: idLinkList<idAI> simpleThinkNode;
// NOSAVE: idAAS* aas;
// NOSAVE: idAASCallback* aasFind;
// NOTE That some AAS stuff is done at end of ::Restore
// Movement
move.Save( savefile );
savedMove.Save( savefile );
savefile->WriteStaticObject( physicsObj );
savefile->WriteBool( GetPhysics() == static_cast<const idPhysics *>(&physicsObj) );
savefile->WriteBool( lastHitCheckResult );
savefile->WriteInt( lastHitCheckTime );
savefile->WriteInt( lastAttackTime );
savefile->WriteFloat( projectile_height_to_distance_ratio );
savefile->WriteInt( attackAnimInfo.Num() );
for( i = 0; i < attackAnimInfo.Num(); i++ ) {
savefile->WriteVec3( attackAnimInfo[ i ].attackOffset );
savefile->WriteVec3( attackAnimInfo[ i ].eyeOffset );
}
// TOSAVE: mutable idClipModel* projectileClipModel;
projectile.Save ( savefile );
// Talking
savefile->WriteInt( chatterTime );
// savefile->WriteInt( chatterRateCombat ); // NOSAVE:
// savefile->WriteInt( chatterRateIdle ); // NOSAVE:
savefile->WriteInt( talkState );
talkTarget.Save( savefile );
savefile->WriteInt( talkMessage );
savefile->WriteInt( talkBusyCount );
savefile->WriteInt ( speakTime );
// Focus
lookTarget.Save ( savefile );
savefile->WriteInt( focusType );
focusEntity.Save ( savefile );
savefile->WriteFloat ( focusRange );
savefile->WriteInt ( focusAlignTime );
savefile->WriteInt ( focusTime );
savefile->WriteVec3( currentFocusPos );
// Looking
savefile->WriteBool( allowJointMod );
savefile->WriteInt( alignHeadTime );
savefile->WriteInt( forceAlignHeadTime );
savefile->WriteAngles( eyeAng );
savefile->WriteAngles( lookAng );
savefile->WriteAngles( destLookAng );
savefile->WriteAngles( lookMin );
savefile->WriteAngles( lookMax );
savefile->WriteInt( lookJoints.Num() );
for( i = 0; i < lookJoints.Num(); i++ ) {
savefile->WriteJoint( lookJoints[ i ] );
savefile->WriteAngles( lookJointAngles[ i ] );
}
savefile->WriteFloat( eyeVerticalOffset );
savefile->WriteFloat( eyeHorizontalOffset );
savefile->WriteFloat( headFocusRate );
savefile->WriteFloat( eyeFocusRate );
// Joint Controllers
savefile->WriteAngles( eyeMin );
savefile->WriteAngles( eyeMax );
savefile->WriteJoint( orientationJoint );
pusher.Save( savefile ); // cnicholson: Added unsaved var
scriptedActionEnt.Save( savefile ); // cnicholson: Added unsaved var
savefile->Write( &aifl, sizeof( aifl ) );
// Misc
savefile->WriteInt ( actionAnimNum );
savefile->WriteInt ( actionTime );
savefile->WriteInt ( actionSkipTime );
savefile->WriteInt ( flagOverrides );
// Combat variables
savefile->Write( &combat.fl, sizeof( combat.fl ) );
savefile->WriteFloat ( combat.max_chasing_turn );
savefile->WriteFloat ( combat.shotAtTime );
savefile->WriteFloat ( combat.shotAtAngle );
savefile->WriteVec2 ( combat.hideRange );
savefile->WriteVec2 ( combat.attackRange );
savefile->WriteInt ( combat.attackSightDelay );
savefile->WriteFloat ( combat.meleeRange );
savefile->WriteFloat ( combat.aggressiveRange );
savefile->WriteFloat ( combat.aggressiveScale );
savefile->WriteInt ( combat.investigateTime );
savefile->WriteFloat ( combat.visStandHeight );
savefile->WriteFloat ( combat.visCrouchHeight );
savefile->WriteFloat ( combat.visRange );
savefile->WriteFloat ( combat.earRange );
savefile->WriteFloat ( combat.awareRange );
savefile->WriteInt ( combat.tacticalMaskAvailable );
savefile->WriteInt ( (int&)combat.tacticalCurrent );
savefile->WriteInt ( combat.tacticalUpdateTime );
savefile->WriteInt ( combat.tacticalPainTaken );
savefile->WriteInt ( combat.tacticalPainThreshold );
savefile->WriteInt ( combat.tacticalFlinches );
savefile->WriteInt ( combat.maxLostVisTime );
savefile->WriteFloat ( combat.threatBase );
savefile->WriteFloat ( combat.threatCurrent );
savefile->WriteInt ( combat.coverValidTime );
savefile->WriteInt ( combat.maxInvalidCoverTime );
// Passive state variables
savefile->WriteString ( passive.prefix );
savefile->WriteString ( passive.animFidgetPrefix );
savefile->WriteString ( passive.animIdlePrefix);
savefile->WriteString ( passive.animTalkPrefix );
savefile->WriteString ( passive.idleAnim );
savefile->WriteInt ( passive.idleAnimChangeTime );
savefile->WriteInt ( passive.fidgetTime );
savefile->WriteInt ( passive.talkTime );
savefile->Write ( &passive.fl, sizeof(passive.fl) );
// Enemy
enemy.ent.Save ( savefile );
savefile->Write ( &enemy.fl, sizeof(enemy.fl) );
savefile->WriteInt ( enemy.lastVisibleChangeTime );
savefile->WriteVec3 ( enemy.lastKnownPosition );
savefile->WriteVec3 ( enemy.smoothedLinearVelocity );
savefile->WriteVec3 ( enemy.smoothedPushedVelocity );
savefile->WriteVec3 ( enemy.lastVisibleEyePosition );
savefile->WriteVec3 ( enemy.lastVisibleChestPosition );
savefile->WriteVec3 ( enemy.lastVisibleFromEyePosition );
savefile->WriteInt ( enemy.lastVisibleTime );
savefile->WriteFloat ( enemy.range );
savefile->WriteFloat ( enemy.range2d );
savefile->WriteInt ( enemy.changeTime );
savefile->WriteInt ( enemy.checkTime );
// Pain variables
savefile->WriteFloat ( pain.threshold );
savefile->WriteFloat ( pain.takenThisFrame );
savefile->WriteInt ( pain.lastTakenTime );
savefile->WriteInt ( pain.loopEndTime );
savefile->WriteString ( pain.loopType );
// Functions
funcs.first_sight.Save ( savefile );
funcs.sight.Save ( savefile );
funcs.pain.Save ( savefile );
funcs.damage.Save ( savefile );
funcs.death.Save ( savefile );
funcs.attack.Save ( savefile );
funcs.init.Save ( savefile );
funcs.onclick.Save ( savefile );
funcs.launch_projectile.Save ( savefile );
funcs.footstep.Save ( savefile );
mPlayback.Save( savefile ); // cnicholson: Added save functionality
mLookPlayback.Save( savefile ); // cnicholson: Added save functionality
// Tactical Sensor
aasSensor->Save( savefile );
// Helpers
tether.Save ( savefile );
helperCurrent.Save ( savefile );
helperIdeal.Save ( savefile );
leader.Save ( savefile );
spawner.Save ( savefile );
// Action timers
actionTimerRangedAttack.Save ( savefile );
actionTimerEvade.Save ( savefile );
actionTimerSpecialAttack.Save ( savefile );
actionTimerPain.Save ( savefile );
// Actions
actionEvadeLeft.Save ( savefile );
actionEvadeRight.Save ( savefile );
actionRangedAttack.Save ( savefile );
actionMeleeAttack.Save ( savefile );
actionLeapAttack.Save ( savefile );
actionJumpBack.Save ( savefile );
}
/*
=====================
idAI::Restore
=====================
*/
void idAI::Restore( idRestoreGame *savefile ) {
bool restorePhysics;
int i;
int num;
idBounds bounds;
InitNonPersistentSpawnArgs ( );
// INTENTIONALLY NOT SAVED: idLinkList<idAI> simpleThinkNode;
// INTENTIONALLY NOT SAVED: idAAS* aas;
// INTENTIONALLY NOT SAVED: idAASCallback* aasFind;
// NOTE That some AAS stuff is done at end of ::Restore
move.Restore( savefile );
savedMove.Restore( savefile );
savefile->ReadStaticObject( physicsObj );
savefile->ReadBool( restorePhysics );
if ( restorePhysics ) {
RestorePhysics( &physicsObj );
}
savefile->ReadBool( lastHitCheckResult );
savefile->ReadInt( lastHitCheckTime );
savefile->ReadInt( lastAttackTime );
savefile->ReadFloat( projectile_height_to_distance_ratio );
savefile->ReadInt( num );
attackAnimInfo.SetGranularity( 1 );
attackAnimInfo.SetNum( num );
for( i = 0; i < num; i++ ) {
savefile->ReadVec3( attackAnimInfo[ i ].attackOffset );
savefile->ReadVec3( attackAnimInfo[ i ].eyeOffset );
}
// TORESTORE: mutable idClipModel* projectileClipModel;
projectile.Restore( savefile );
// Talking
savefile->ReadInt( chatterTime );
// savefile->ReadInt( chatterRateCombat ); // Don't save
// savefile->ReadInt( chatterRateIdle ); // Don't save
savefile->ReadInt( i );
talkState = static_cast<talkState_t>( i );
talkTarget.Restore( savefile );
savefile->ReadInt( i );
talkMessage = static_cast<talkMessage_t>( i );
savefile->ReadInt( talkBusyCount );
savefile->ReadInt ( speakTime );
// Focus
lookTarget.Restore ( savefile );
savefile->ReadInt( i );
focusType = static_cast<aiFocus_t>( i );
focusEntity.Restore ( savefile );
savefile->ReadFloat ( focusRange );
savefile->ReadInt ( focusAlignTime );
savefile->ReadInt ( focusTime );
savefile->ReadVec3( currentFocusPos );
// Looking
savefile->ReadBool( allowJointMod );
savefile->ReadInt( alignHeadTime );
savefile->ReadInt( forceAlignHeadTime );
savefile->ReadAngles( eyeAng );
savefile->ReadAngles( lookAng );
savefile->ReadAngles( destLookAng );
savefile->ReadAngles( lookMin );
savefile->ReadAngles( lookMax );
savefile->ReadInt( num );
lookJoints.SetGranularity( 1 );
lookJoints.SetNum( num );
lookJointAngles.SetGranularity( 1 );
lookJointAngles.SetNum( num );
for( i = 0; i < num; i++ ) {
savefile->ReadJoint( lookJoints[ i ] );
savefile->ReadAngles( lookJointAngles[ i ] );
}
savefile->ReadFloat( eyeVerticalOffset );
savefile->ReadFloat( eyeHorizontalOffset );
savefile->ReadFloat( headFocusRate );
savefile->ReadFloat( eyeFocusRate );
// Joint Controllers
savefile->ReadAngles( eyeMin );
savefile->ReadAngles( eyeMax );
savefile->ReadJoint( orientationJoint );
pusher.Restore( savefile ); // cnicholson: Added unrestored var
scriptedActionEnt.Restore ( savefile ); // cnicholson: Added unrestored var
savefile->Read( &aifl, sizeof( aifl ) );
// Misc
savefile->ReadInt ( actionAnimNum );
savefile->ReadInt ( actionTime );
savefile->ReadInt ( actionSkipTime );
savefile->ReadInt ( flagOverrides );
// Combat variables
savefile->Read ( &combat.fl, sizeof( combat.fl ) );
savefile->ReadFloat ( combat.max_chasing_turn );
savefile->ReadFloat ( combat.shotAtTime );
savefile->ReadFloat ( combat.shotAtAngle );
savefile->ReadVec2 ( combat.hideRange );
savefile->ReadVec2 ( combat.attackRange );
savefile->ReadInt ( combat.attackSightDelay );
savefile->ReadFloat ( combat.meleeRange );
savefile->ReadFloat ( combat.aggressiveRange );
savefile->ReadFloat ( combat.aggressiveScale );
savefile->ReadInt ( combat.investigateTime );
savefile->ReadFloat ( combat.visStandHeight );
savefile->ReadFloat ( combat.visCrouchHeight );
savefile->ReadFloat ( combat.visRange );
savefile->ReadFloat ( combat.earRange );
savefile->ReadFloat ( combat.awareRange );
savefile->ReadInt ( combat.tacticalMaskAvailable );
savefile->ReadInt ( (int&)combat.tacticalCurrent );
savefile->ReadInt ( combat.tacticalUpdateTime );
savefile->ReadInt ( combat.tacticalPainTaken );
savefile->ReadInt ( combat.tacticalPainThreshold );
savefile->ReadInt ( combat.tacticalFlinches );
savefile->ReadInt ( combat.maxLostVisTime );
savefile->ReadFloat ( combat.threatBase );
savefile->ReadFloat ( combat.threatCurrent );
savefile->ReadInt ( combat.coverValidTime );
savefile->ReadInt ( combat.maxInvalidCoverTime );
// Passive state variables
savefile->ReadString ( passive.prefix );
savefile->ReadString ( passive.animFidgetPrefix );
savefile->ReadString ( passive.animIdlePrefix);
savefile->ReadString ( passive.animTalkPrefix );
savefile->ReadString ( passive.idleAnim );
savefile->ReadInt ( passive.idleAnimChangeTime );
savefile->ReadInt ( passive.fidgetTime );
savefile->ReadInt ( passive.talkTime );
savefile->Read ( &passive.fl, sizeof(passive.fl) );
// Enemy
enemy.ent.Restore ( savefile );
savefile->Read ( &enemy.fl, sizeof(enemy.fl) );
savefile->ReadInt ( enemy.lastVisibleChangeTime );
savefile->ReadVec3 ( enemy.lastKnownPosition );
savefile->ReadVec3 ( enemy.smoothedLinearVelocity );
savefile->ReadVec3 ( enemy.smoothedPushedVelocity );
savefile->ReadVec3 ( enemy.lastVisibleEyePosition );
savefile->ReadVec3 ( enemy.lastVisibleChestPosition );
savefile->ReadVec3 ( enemy.lastVisibleFromEyePosition );
savefile->ReadInt ( enemy.lastVisibleTime );
savefile->ReadFloat ( enemy.range );
savefile->ReadFloat ( enemy.range2d );
savefile->ReadInt ( enemy.changeTime );
savefile->ReadInt ( enemy.checkTime );
// Pain variables
savefile->ReadFloat ( pain.threshold );
savefile->ReadFloat ( pain.takenThisFrame );
savefile->ReadInt ( pain.lastTakenTime );
savefile->ReadInt ( pain.loopEndTime );
savefile->ReadString ( pain.loopType );
// Functions
funcs.first_sight.Restore ( savefile );
funcs.sight.Restore ( savefile );
funcs.pain.Restore ( savefile );
funcs.damage.Restore ( savefile );
funcs.death.Restore ( savefile );
funcs.attack.Restore ( savefile );
funcs.init.Restore ( savefile );
funcs.onclick.Restore ( savefile );
funcs.launch_projectile.Restore ( savefile );
funcs.footstep.Restore ( savefile );
mPlayback.Restore( savefile ); // cnicholson: Added restore functionality
mLookPlayback.Restore( savefile ); // cnicholson: Added restore functionality
// Tactical Sensor
aasSensor->Restore( savefile );
// Helpers
tether.Restore ( savefile );
helperCurrent.Restore( savefile );
helperIdeal.Restore ( savefile );
leader.Restore ( savefile );
spawner.Restore ( savefile );
// Action timers
actionTimerRangedAttack.Restore ( savefile );
actionTimerEvade.Restore ( savefile );
actionTimerSpecialAttack.Restore ( savefile );
actionTimerPain.Restore ( savefile );
// Actions
actionEvadeLeft.Restore ( savefile );
actionEvadeRight.Restore ( savefile );
actionRangedAttack.Restore ( savefile );
actionMeleeAttack.Restore ( savefile );
actionLeapAttack.Restore ( savefile );
actionJumpBack.Restore ( savefile );
// Set the AAS if the character has the correct gravity vector
idVec3 gravity = spawnArgs.GetVector( "gravityDir", "0 0 -1" );
gravity *= g_gravity.GetFloat();
if ( gravity == gameLocal.GetGravity() ) {
SetAAS();
}
// create combat collision hull for exact collision detection, do initial set un-hidden
SetCombatModel();
LinkCombat();
}
/*
=====================
idAI::InitNonPersistentSpawnArgs
=====================
*/
void idAI::InitNonPersistentSpawnArgs ( void ) {
aasSensor = rvAASTacticalSensor::CREATE_SENSOR(this);
aasFind = NULL;
simpleThinkNode.Remove ( );
simpleThinkNode.SetOwner ( this );
chatterRateIdle = SEC2MS ( spawnArgs.GetFloat ( "chatter_rate_idle", "0" ) );
chatterRateCombat = SEC2MS ( spawnArgs.GetFloat ( "chatter_rate_combat", "0" ) );
chatterTime = 0;
combat.tacticalMaskUpdate = 0;
enemy.smoothVelocityRate = spawnArgs.GetFloat ( "smoothVelocityRate", "0.1" );
}
/*
=====================
idAI::Spawn
=====================
*/
void idAI::Spawn( void ) {
const char* jointname;
const idKeyValue* kv;
idStr jointName;
idAngles jointScale;
jointHandle_t joint;
idVec3 local_dir;
// Are all monsters disabled?
if ( !g_monsters.GetBool() ) {
PostEventMS( &EV_Remove, 0 );
return;
}
// Initialize the non saved spawn args
InitNonPersistentSpawnArgs ( );
spawnArgs.GetInt( "team", "1", team );
spawnArgs.GetInt( "rank", "0", rank );
animPrefix = spawnArgs.GetString ( "animPrefix", "" );
// Standard flags
fl.notarget = spawnArgs.GetBool ( "notarget", "0" );
fl.quickBurn = false;
// AI flags
flagOverrides = 0;
memset ( &aifl, 0, sizeof(aifl) );
aifl.ignoreFlashlight = spawnArgs.GetBool ( "ignore_flashlight", "1" );
aifl.lookAtPlayer = spawnArgs.GetBool ( "lookAtPlayer", "0" );
aifl.disableLook = spawnArgs.GetBool ( "noLook", "0" );
aifl.undying = spawnArgs.GetBool ( "undying", "0" );
aifl.killerGuard = spawnArgs.GetBool ( "killer_guard", "0" );
aifl.scriptedEndWithIdle = true;
// Setup Move Data
move.Spawn( spawnArgs );
pain.threshold = spawnArgs.GetInt ( "painThreshold", "0" );
pain.takenThisFrame = 0;
// Initialize combat variables
combat.fl.aware = spawnArgs.GetBool ( "ambush", "0" );
combat.fl.tetherNoBreak = spawnArgs.GetBool ( "tetherNoBreak", "0" );
combat.fl.noChatter = spawnArgs.GetBool ( "noCombatChatter" );
combat.hideRange = spawnArgs.GetVec2 ( "hideRange", "150 750" );
combat.attackRange = spawnArgs.GetVec2 ( "attackRange", "0 1000" );
combat.attackSightDelay = SEC2MS ( spawnArgs.GetFloat ( "attackSightDelay", "1" ) );
combat.visRange = spawnArgs.GetFloat( "visRange", "2048" );
combat.visStandHeight = spawnArgs.GetFloat( "visStandHeight", "68" );
combat.visCrouchHeight = spawnArgs.GetFloat( "visCrouchHeight", "48" );
combat.earRange = spawnArgs.GetFloat( "earRange", "2048" );
combat.awareRange = spawnArgs.GetFloat( "awareRange", "150" );
combat.aggressiveRange = spawnArgs.GetFloat( "aggressiveRange", "0" );
combat.maxLostVisTime = SEC2MS ( spawnArgs.GetFloat ( "maxLostVisTime", "10" ) );
combat.tacticalPainThreshold = spawnArgs.GetInt ( "tactical_painThreshold", va("%d", health / 4) );
combat.coverValidTime = 0;
combat.maxInvalidCoverTime = SEC2MS ( spawnArgs.GetFloat ( "maxInvalidCoverTime", "1" ) );
combat.threatBase = spawnArgs.GetFloat ( "threatBase", "1" );
combat.threatCurrent = combat.threatBase;
SetPassivePrefix ( spawnArgs.GetString ( "passivePrefix" ) );
disablePain = spawnArgs.GetBool ( "nopain", "0" );
spawnArgs.GetFloat( "melee_range", "64", combat.meleeRange );
spawnArgs.GetFloat( "projectile_height_to_distance_ratio", "1", projectile_height_to_distance_ratio );
//melee superhero -- take far reduced damage from melee.
if ( spawnArgs.GetString( "objectivetitle_failed", NULL ) && spawnArgs.GetBool( "meleeSuperhero", "1") ) {
aifl.meleeSuperhero = true;
} else {
aifl.meleeSuperhero = false;
}
//announce rate
announceRate = spawnArgs.GetFloat( "announceRate" );
if( 0.0f == announceRate ) {
announceRate = AISPEAK_CHANCE;
}
fl.takedamage = !spawnArgs.GetBool( "noDamage" );
animator.RemoveOriginOffset( true );
// create combat collision hull for exact collision detection
SetCombatModel();
lookMin = spawnArgs.GetAngles( "look_min", "-80 -75 0" );
lookMax = spawnArgs.GetAngles( "look_max", "80 75 0" );
lookJoints.SetGranularity( 1 );
lookJointAngles.SetGranularity( 1 );
kv = spawnArgs.MatchPrefix( "look_joint", NULL );
while( kv ) {
jointName = kv->GetKey();
jointName.StripLeadingOnce( "look_joint " );
joint = animator.GetJointHandle( jointName );
if ( joint == INVALID_JOINT ) {
gameLocal.Warning( "Unknown look_joint '%s' on entity %s", jointName.c_str(), name.c_str() );
} else {
jointScale = spawnArgs.GetAngles( kv->GetKey(), "0 0 0" );
jointScale.roll = 0.0f;
// if no scale on any component, then don't bother adding it. this may be done to
// zero out rotation from an inherited entitydef.
if ( jointScale != ang_zero ) {
lookJoints.Append( joint );
lookJointAngles.Append( jointScale );
}
}
kv = spawnArgs.MatchPrefix( "look_joint", kv );
}
// calculate joint positions on attack frames so we can do proper "can hit" tests
CalculateAttackOffsets();
eyeMin = spawnArgs.GetAngles( "eye_turn_min", "-10 -30 0" );
eyeMax = spawnArgs.GetAngles( "eye_turn_max", "10 30 0" );
eyeVerticalOffset = spawnArgs.GetFloat( "eye_verticle_offset", "5" );
eyeHorizontalOffset = spawnArgs.GetFloat( "eye_horizontal_offset", "-8" );
headFocusRate = spawnArgs.GetFloat( "head_focus_rate", "0.06" );
eyeFocusRate = spawnArgs.GetFloat( "eye_focus_rate", "0.5" );
focusRange = spawnArgs.GetFloat( "focus_range", "0" );
focusAlignTime = SEC2MS( spawnArgs.GetFloat( "focus_align_time", "1" ) );
jointname = spawnArgs.GetString( "joint_orientation" );
if ( *jointname ) {
orientationJoint = animator.GetJointHandle( jointname );
if ( orientationJoint == INVALID_JOINT ) {
gameLocal.Warning( "Joint '%s' not found on '%s'", jointname, name.c_str() );
}
}
jointname = spawnArgs.GetString( "joint_flytilt" );
if ( *jointname ) {
move.flyTiltJoint = animator.GetJointHandle( jointname );
if ( move.flyTiltJoint == INVALID_JOINT ) {
gameLocal.Warning( "Joint '%s' not found on '%s'", jointname, name.c_str() );
}
}
physicsObj.SetSelf( this );
physicsObj.SetClipModel( new idClipModel( GetPhysics()->GetClipModel() ), 1.0f );
physicsObj.SetMass( spawnArgs.GetFloat( "mass", "100" ) );
if ( spawnArgs.GetBool( "big_monster" ) ) {
physicsObj.SetContents( 0 );
physicsObj.SetClipMask( MASK_MONSTERSOLID & ~CONTENTS_BODY );
} else {
if ( use_combat_bbox ) {
physicsObj.SetContents( CONTENTS_BODY|CONTENTS_SOLID );
} else {
physicsObj.SetContents( CONTENTS_BODY );
}
physicsObj.SetClipMask( MASK_MONSTERSOLID );
}
// move up to make sure the monster is at least an epsilon above the floor
physicsObj.SetOrigin( GetPhysics()->GetOrigin() + idVec3( 0, 0, CM_CLIP_EPSILON ) );
idVec3 gravity = spawnArgs.GetVector( "gravityDir", "0 0 -1" );
gravity *= g_gravity.GetFloat();
physicsObj.SetGravity( gravity );
SetPhysics( &physicsObj );
physicsObj.GetGravityAxis().ProjectVector( viewAxis[ 0 ], local_dir );
move.current_yaw = local_dir.ToYaw();
move.ideal_yaw = idMath::AngleNormalize180( move.current_yaw );
lookAng.Zero ( );
lookAng.yaw = move.current_yaw;
SetAAS();
projectile = NULL;
projectileClipModel = NULL;
if ( spawnArgs.GetBool( "hide" ) || spawnArgs.GetBool( "trigger_anim" ) ) {
fl.takedamage = false;
physicsObj.SetContents( 0 );
physicsObj.GetClipModel()->Unlink();
Hide();
} else {
// play a looping ambient sound if we have one
StartSound( "snd_ambient", SND_CHANNEL_AMBIENT, 0, false, NULL );
}
if ( health <= 0 ) {
gameLocal.Warning( "entity '%s' doesn't have health set", name.c_str() );
health = 1;
}
BecomeActive( TH_THINK );
if ( move.fl.allowPushMovables ) {
af.SetupPose( this, gameLocal.time );
af.GetPhysics()->EnableClip();
}
// init the move variables
StopMove( MOVE_STATUS_DONE );
// Initialize any scripts
idStr prefix = "script_";
for ( kv = spawnArgs.MatchPrefix ( prefix.c_str(), NULL );
kv;
kv = spawnArgs.MatchPrefix ( prefix.c_str(), kv ) ) {
SetScript ( kv->GetKey().c_str() + prefix.Length(), kv->GetValue() );
}
// Initialize actions
actionEvadeLeft.Init ( spawnArgs, "action_evadeLeft", NULL, 0 );
actionEvadeRight.Init ( spawnArgs, "action_evadeRight", NULL, 0 );
actionRangedAttack.Init ( spawnArgs, "action_rangedAttack", NULL, AIACTIONF_ATTACK );
actionMeleeAttack.Init ( spawnArgs, "action_meleeAttack", NULL, AIACTIONF_ATTACK|AIACTIONF_MELEE );
actionLeapAttack.Init ( spawnArgs, "action_leapAttack", NULL, AIACTIONF_ATTACK );
actionJumpBack.Init ( spawnArgs, "action_jumpBack", NULL, 0 );
// Global action timers
actionTimerRangedAttack.Init ( spawnArgs, "actionTimer_RangedAttack" );
actionTimerEvade.Init ( spawnArgs, "actionTimer_Evade" );
actionTimerSpecialAttack.Init ( spawnArgs, "actionTimer_SpecialAttack" );
actionTimerPain.Init ( spawnArgs, "actionTimer_pain" );
// Available tactical options
combat.tacticalUpdateTime = 0;
combat.tacticalCurrent = AITACTICAL_NONE;
combat.tacticalMaskUpdate = 0;
combat.tacticalMaskAvailable = AITACTICAL_TURRET_BIT|AITACTICAL_MOVE_FOLLOW_BIT|AITACTICAL_MOVE_TETHER_BIT;
combat.tacticalMaskAvailable |= (spawnArgs.GetBool ( "tactical_cover", "0" ) ? AITACTICAL_COVER_BITS : 0);
combat.tacticalMaskAvailable |= (spawnArgs.GetBool ( "tactical_ranged", "0" ) ? AITACTICAL_RANGED_BITS : 0);
combat.tacticalMaskAvailable |= (spawnArgs.GetBool ( "tactical_hide", "0" ) ? AITACTICAL_HIDE_BIT : 0);
combat.tacticalMaskAvailable |= (spawnArgs.GetBool ( "tactical_rush", "0" ) ? AITACTICAL_MELEE_BIT : 0);
combat.tacticalMaskAvailable |= (spawnArgs.GetBool ( "tactical_passive", "0" ) ? AITACTICAL_PASSIVE_BIT : 0);
combat.tacticalMaskAvailable |= (spawnArgs.GetBool ( "allowPlayerPush", "0" ) ? AITACTICAL_MOVE_PLAYERPUSH_BIT : 0);
// Talking?
const char* npc;
if ( spawnArgs.GetString( "npc_name", NULL, &npc ) != NULL && *npc ) {
if ( spawnArgs.GetBool ( "follows", "0" ) ) {
SetTalkState ( TALK_FOLLOW );
} else {
switch ( spawnArgs.GetInt( "talks" ) )
{
case 2:
SetTalkState ( TALK_WAIT );
break;
case 1:
SetTalkState ( TALK_OK );
break;
case 0:
default:
SetTalkState ( TALK_BUSY );
break;
}
}
} else {
SetTalkState ( TALK_NEVER );
}
// Passive or aggressive ai?
if ( spawnArgs.GetBool ( "passive" ) ) {
Event_BecomePassive ( true );
if ( spawnArgs.GetInt ( "passive" ) > 1 ) {
aifl.disableLook = true;
}
}
// Print out a warning about any AI that is spawned unhidden since they will be all thinking
if( gameLocal.GameState ( ) == GAMESTATE_STARTUP && !spawnArgs.GetInt( "hide" ) && !spawnArgs.GetInt( "trigger_anim" ) && !spawnArgs.GetInt( "trigger_cover" ) && !spawnArgs.GetInt( "trigger_move" ) ){
gameLocal.Warning( "Unhidden AI placed in map (will be constantly active): %s (%s)", name.c_str(), GetPhysics()->GetOrigin().ToString() );
}
Begin ( );
// RAVEN BEGIN
// twhitaker: needed this for difficulty settings
PostEventMS( &EV_PostSpawn, 0 );
// RAVEN END
}
/*
===================
idAI::Begin
===================
*/
void idAI::Begin ( void ) {
const char* temp;
bool animWalk;
bool animRun;
// Look for the lack of a run or walk animation and force the opposite
animWalk = HasAnim ( ANIMCHANNEL_LEGS, "walk" );
animRun = HasAnim ( ANIMCHANNEL_LEGS, "run" );
if ( !animWalk && animRun ) {
move.fl.noWalk = true;
} else if ( !animRun && animWalk ) {
move.fl.noRun = true;
}
// If a trigger anim or hide is specified then wait until activated before waking up
if ( spawnArgs.GetString ( "trigger_anim", "", &temp ) || spawnArgs.GetBool ( "hide" ) ) {
Hide();
PostState ( "Wait_Activated" );
PostState ( "State_WakeUp", SEC2MS(spawnArgs.GetFloat ( "wait" ) ) );
// Wake up
} else {
PostState ( "State_WakeUp", SEC2MS(spawnArgs.GetFloat ( "wait" ) ) );
}
}
/*
===================
idAI::WakeUp
===================
*/
void idAI::WakeUp ( void ) {
const char* temp;
// Already awake?
if ( aifl.awake ) {
return;
}
// Find the closest helper
UpdateHelper ( );
aifl.awake = true;
// If the monster is flying then start them with the flying movement type
if ( spawnArgs.GetBool ( "static" ) ) {
SetMoveType ( MOVETYPE_STATIC );
} else if ( spawnArgs.GetBool ( "flying" ) ) {
SetMoveType ( MOVETYPE_FLY );
move.moveDest = physicsObj.GetOrigin();
move.moveDest.z += move.fly_offset;
} else {
SetMoveType ( MOVETYPE_ANIM );
}
// Wake up any linked entities
WakeUpTargets ( );
// Default enemy?
if ( !combat.fl.ignoreEnemies ) {
if ( spawnArgs.GetString ( "enemy", "", &temp ) && *temp ) {
SetEnemy ( gameLocal.FindEntity ( temp ) );
} else if ( spawnArgs.GetBool ( "forceEnemy", "0" ) ) {
SetEnemy ( FindEnemy ( false, true ) );
}
}
// Default leader?
if ( spawnArgs.GetString ( "leader", "", &temp ) && *temp ) {
SetLeader ( gameLocal.FindEntity ( temp ) );
}
// If hidden and face enemy is specified we should orient ourselves toward our enemy immediately
if ( enemy.ent && IsHidden ( ) && spawnArgs.GetBool ( "faceEnemy" ) ) {
TurnToward ( enemy.ent->GetPhysics()->GetOrigin() );
move.current_yaw = move.ideal_yaw;
viewAxis = idAngles( 0, move.current_yaw, 0 ).ToMat3();
}
Show ( );
aiManager.AddTeammate ( this );
// External script functions
ExecScriptFunction( funcs.init, this );
OnWakeUp ( );
}
/*
===================
idAI::List_f
===================
*/
void idAI::List_f( const idCmdArgs &args ) {
int e;
int i;
idEntity* check;
int count;
idDict countsFixed;
idDict countsSpawned;
count = 0;
gameLocal.Printf( "%-4s %-20s %s\n", " Num", "EntityDef", "Name" );
gameLocal.Printf( "------------------------------------------------\n" );
for( e = 0; e < MAX_GENTITIES; e++ ) {
check = gameLocal.entities[ e ];
if ( !check ) {
continue;
}
if ( check->IsType ( idAI::Type ) ) {
idAI* checkAI = static_cast<idAI*>(check);
// Skip spawned AI
if ( checkAI->spawner ) {
continue;
}
countsFixed.SetInt ( check->GetEntityDefName(), countsFixed.GetInt ( check->GetEntityDefName(), "0" ) + 1 );
gameLocal.Printf( "%4i: %-20s %-20s move: %d\n", e, check->GetEntityDefName(), check->name.c_str(), checkAI->move.fl.allowAnimMove );
count++;
} else if ( check->IsType ( rvSpawner::GetClassType() ) ) {
const idKeyValue* kv;
rvSpawner* checkSpawner = static_cast<rvSpawner*>(check);
for ( kv = check->spawnArgs.MatchPrefix( "def_spawn", NULL ); kv; kv = check->spawnArgs.MatchPrefix( "def_spawn", kv ) ) {
countsSpawned.SetInt ( kv->GetValue(), countsSpawned.GetInt ( kv->GetValue(), "0" ) + 1 );
}
for ( i = 0; i < checkSpawner->GetNumSpawnPoints(); i ++ ) {
check = checkSpawner->GetSpawnPoint ( i );
if ( !check ) {
continue;
}
for ( kv = check->spawnArgs.MatchPrefix( "def_spawn", NULL ); kv; kv = check->spawnArgs.MatchPrefix( "def_spawn", kv ) ) {
countsSpawned.SetInt ( kv->GetValue(), countsSpawned.GetInt ( kv->GetValue(), "0" ) + 1 );
}
}
}
}
// Combine the two lists
for ( e = 0; e < countsSpawned.GetNumKeyVals(); e ++ ) {
const char* keyName = countsSpawned.GetKeyVal ( e )->GetKey();
countsFixed.Set ( keyName, va("(%s) %3d",
countsSpawned.GetKeyVal ( e )->GetValue().c_str(),
countsFixed.GetInt ( keyName, "0" ) + atoi(countsSpawned.GetKeyVal ( e )->GetValue()) ) );
}
// Print out total counts
if ( countsFixed.GetNumKeyVals() ) {
gameLocal.Printf( "------------------------------------------------\n" );
for ( e = 0; e < countsFixed.GetNumKeyVals(); e ++ ) {
const idKeyValue* kv = countsFixed.GetKeyVal ( e );
gameLocal.Printf( "%10s: %-20s\n", kv->GetValue().c_str(), kv->GetKey().c_str() );
}
}
gameLocal.Printf( "------------------------------------------------\n" );
gameLocal.Printf( "...%d monsters (%d unique types)\n", count, countsFixed.GetNumKeyVals() );
}
/*
================
idAI::DormantBegin
called when entity becomes dormant
================
*/
void idAI::DormantBegin( void ) {
// since dormant happens on a timer, we wont get to update particles to
// hidden through the think loop, but we need to hide them though.
if ( enemyNode.InList() ) {
// remove ourselves from the enemy's enemylist
enemyNode.Remove();
}
idActor::DormantBegin();
}
/*
================
idAI::DormantEnd
called when entity wakes from being dormant
================
*/
void idAI::DormantEnd( void ) {
if ( enemy.ent && !enemyNode.InList() ) {
// let our enemy know we're back on the trail
idActor *enemyEnt = dynamic_cast< idActor *>( enemy.ent.GetEntity() );
if( enemyEnt ){
enemyNode.AddToEnd( enemyEnt->enemyList );
}
}
idActor::DormantEnd();
}
/*
=====================
idAI::ValidateCover
Validate the reserved cover feature with current conditions. Also quickly tests if
cover does not face enemy at all, which triggers an instant update (ignoring maxCoverInvalidTime)
=====================
*/
bool idAI::ValidateCover( ) {
if ( !InCoverMode( ) ) {
return false;
}
if ( aasSensor->TestValidWithCurrentState() && (!enemy.ent || enemy.range > combat.awareRange ) ) {
combat.coverValidTime = gameLocal.time;
} else if ( combat.coverValidTime && !IsCoverValid ( ) ) {
combat.coverValidTime = 0;
OnCoverInvalidated();
}
return IsCoverValid();
}
/*
=====================
idAI::DoDormantTests
=====================
*/
bool idAI::DoDormantTests ( void ) {
// If in a scripted move that we should never go dormant in
if ( aifl.scripted && aifl.scriptedNeverDormant ) {
return false;
}
// If following a player never go dormant
if ( leader && leader->IsType ( idPlayer::GetClassType ( ) ) ) {
return false;
}
// AI should no longer go dormant when outside of tether
if ( IsTethered () && !IsWithinTether ( ) ) {
return false;
}
return idActor::DoDormantTests ( );
}
/*
=====================
idAI::Think
=====================
*/
void idAI::Think( void ) {
// if we are completely closed off from the player, don't do anything at all
if ( CheckDormant() ) {
return;
}
// Simple think this frame?
aifl.simpleThink = aiManager.IsSimpleThink ( this );
aiManager.thinkCount++;
// AI Speeds
if ( ai_speeds.GetBool ( ) ) {
aiManager.timerThink.Start ( );
}
if ( thinkFlags & TH_THINK ) {
// clear out the enemy when he dies or is hidden
idEntity* enemyEnt = enemy.ent;
idActor* enemyAct = dynamic_cast<idActor*>( enemyEnt );
// Clear our enemy if necessary
if ( enemyEnt ) {
if (enemyAct && enemyAct->IsInVehicle()) {
SetEnemy(enemyAct->GetVehicleController().GetVehicle()); // always get angry at the enemy's vehicle first, not the enemy himself
} else {
bool enemyDead = (enemyEnt->fl.takedamage && enemyEnt->health <= 0);
if ( enemyDead || enemyEnt->fl.notarget || enemyEnt->IsHidden() || (enemyAct && enemyAct->team == team)) {
ClearEnemy ( enemyDead );
}
}
}
// Action time is stopped when the torso is not idle
if ( !aifl.action ) {
actionTime += gameLocal.msec;
}
ValidateCover();
move.current_yaw += deltaViewAngles.yaw;
move.ideal_yaw = idMath::AngleNormalize180( move.ideal_yaw + deltaViewAngles.yaw );
deltaViewAngles.Zero();
if( move.moveType != MOVETYPE_PLAYBACK ){
viewAxis = idAngles( 0, move.current_yaw, 0 ).ToMat3();
}
if ( !move.fl.allowHiddenMove && IsHidden() ) {
// hidden monsters
UpdateStates ();
} else if( !ai_freeze.GetBool() ) {
Prethink();
// clear the ik before we do anything else so the skeleton doesn't get updated twice
walkIK.ClearJointMods();
// update enemy position if not dead
if ( !aifl.dead ) {
UpdateEnemy ( );
}
// update state machine
UpdateStates();
// run all movement commands
Move();
// if not dead, chatter and blink
if( move.moveType != MOVETYPE_DEAD ){
UpdateChatter();
CheckBlink();
}
Postthink();
} else {
DrawTactical ( );
}
// clear pain flag so that we recieve any damage between now and the next time we run the script
aifl.pain = false;
aifl.damage = false;
aifl.pushed = false;
pusher = NULL;
} else if ( thinkFlags & TH_PHYSICS ) {
RunPhysics();
}
if ( move.fl.allowPushMovables ) {
PushWithAF();
}
if ( fl.hidden && move.fl.allowHiddenMove ) {
// UpdateAnimation won't call frame commands when hidden, so call them here when we allow hidden movement
animator.ServiceAnims( gameLocal.previousTime, gameLocal.time );
}
aasSensor->Update();
UpdateAnimation();
Present();
LinkCombat();
// AI Speeds
if ( ai_speeds.GetBool ( ) ) {
aiManager.timerThink.Stop ( );
}
}
/*
============
idAI::UpdateFocus
============
*/
void idAI::UpdateFocus ( const idMat3& orientationAxis ) {
// Alwasy look at enemy
if ( !allowJointMod || !allowEyeFocus ) {
SetFocus ( AIFOCUS_NONE, 0 );
} else if ( IsBehindCover() && !aifl.action && !IsSpeaking() ) {
SetFocus ( AIFOCUS_NONE, 0 );
} else if ( !aifl.scripted && (IsEnemyRecentlyVisible(2.0f) || gameLocal.GetTime()-lastAttackTime<1000) ) {
//not scripted and: have an enemy OR we shot at an enemy recently (for when we kill an enemy in the middle of a volley)
SetFocus ( AIFOCUS_ENEMY, 1000 );
} else if ( move.fl.moving && InCoverMode ( ) && DistanceTo ( aasSensor->ReservedOrigin() ) < move.walkRange * 2.0f ) {
SetFocus ( AIFOCUS_COVER, 1000 );
} else if ( InLookAtCoverMode() ) {
SetFocus ( AIFOCUS_COVERLOOK, 1000 );
} else if ( talkTarget && gameLocal.time < passive.talkTime ) {
SetFocus ( AIFOCUS_TALK, 100 );
} else if ( lookTarget ) {
SetFocus ( AIFOCUS_TARGET, 100 );
} else if ( !IsBehindCover() && tether && DistanceTo ( move.moveDest ) < move.walkRange * 2.0f ) {
SetFocus ( AIFOCUS_TETHER, 100 );
} else if ( IsTethered() && (move.fl.moving || IsBehindCover()) ) {
//tethered and at cover or moving - don't look at leader or player
SetFocus ( AIFOCUS_NONE, 0 );
} else if ( helperCurrent && helperCurrent->IsCombat ( ) ) {
SetFocus ( AIFOCUS_HELPER, 100 );
} else if ( !aifl.scripted && !move.fl.moving ) {
idEntity* lookat = NULL;
aiFocus_t newfocus = AIFOCUS_NONE;
if ( leader ) {
idVec3 dir2Leader = leader->GetPhysics()->GetOrigin()-GetPhysics()->GetOrigin();
if ( (viewAxis[0] * dir2Leader) > 0.0f ) {
//leader is in front of me
newfocus = AIFOCUS_LEADER;
lookat = leader;
} else if ( focusType == AIFOCUS_LEADER ) {
//stop looking at him!
SetFocus ( AIFOCUS_NONE, 0 );
}
} else if ( aifl.lookAtPlayer && !move.fl.moving ) {
newfocus = AIFOCUS_PLAYER;
lookat = gameLocal.GetLocalPlayer();
}
if ( newfocus != AIFOCUS_NONE && lookat ) {
if ( DistanceTo ( lookat ) < focusRange * (move.fl.moving?2.0f:1.0f) && CheckFOV ( lookat->GetPhysics()->GetOrigin() ) ) {
SetFocus ( newfocus, 1000 );
}
}
}
// Make sure cover look wasnt invalidated
if ( focusType == AIFOCUS_COVERLOOK && !aasSensor->Look() ) {
focusType = AIFOCUS_NONE;
} else if ( focusType == AIFOCUS_COVER && !aasSensor->Reserved ( ) ) {
focusType = AIFOCUS_NONE;
} else if ( focusType == AIFOCUS_HELPER && !helperCurrent ) {
focusType = AIFOCUS_NONE;
} else if ( focusType == AIFOCUS_TETHER && !tether ) {
focusType = AIFOCUS_NONE;
}
// Update focus type to none when the focus time runs out
if ( focusType != AIFOCUS_NONE ) {
if ( gameLocal.time > focusTime ) {
focusType = AIFOCUS_NONE;
}
}
// Calculate the focus position
if ( focusType == AIFOCUS_NONE ) {
currentFocusPos = GetEyePosition() + orientationAxis[ 0 ] * 64.0f;
} else if ( focusType == AIFOCUS_COVER ) {
currentFocusPos = GetEyePosition() + aasSensor->Reserved()->Normal() * 64.0f;
} else if ( focusType == AIFOCUS_COVERLOOK ) {
currentFocusPos = GetEyePosition() + aasSensor->Look()->Normal() * 64.0f;
} else if ( focusType == AIFOCUS_ENEMY ) {
currentFocusPos = enemy.lastVisibleEyePosition;
} else if ( focusType == AIFOCUS_HELPER ) {
currentFocusPos = helperCurrent->GetPhysics()->GetOrigin() + helperCurrent->GetDirection ( this );
} else if ( focusType == AIFOCUS_TETHER ) {
currentFocusPos = GetEyePosition() + tether->GetPhysics()->GetAxis()[0] * 64.0f;
} else {
idEntity* focusEnt = NULL;
switch ( focusType ) {
case AIFOCUS_LEADER: focusEnt = leader; break;
case AIFOCUS_PLAYER: focusEnt = gameLocal.GetLocalPlayer(); break;
case AIFOCUS_TALK: focusEnt = talkTarget; break;
case AIFOCUS_TARGET: focusEnt = lookTarget; break;
}
if ( focusEnt ) {
if ( focusEnt->IsType ( idActor::GetClassType ( ) ) ) {
currentFocusPos = static_cast<idActor*>(focusEnt)->GetEyePosition() - eyeVerticalOffset * focusEnt->GetPhysics()->GetGravityNormal();
} else {
currentFocusPos = focusEnt->GetPhysics()->GetOrigin();
}
} else {
currentFocusPos = GetEyePosition() + orientationAxis[ 0 ] * 64.0f;
}
}
//draw it so we can see what they think they're looking at!
if ( DebugFilter(ai_debugMove) ) { // Cyan & Blue = currentFocusPos
if ( focusType != AIFOCUS_NONE ) {
gameRenderWorld->DebugArrow( colorCyan, GetEyePosition(), currentFocusPos, 0 );
} else {
gameRenderWorld->DebugArrow( colorBlue, GetEyePosition(), currentFocusPos, 0 );
}
}
}
/*
=====================
idAI::UpdateStates
=====================
*/
void idAI::UpdateStates ( void ) {
MEM_SCOPED_TAG(tag,MA_DEFAULT);
// Continue updating tactical state if for some reason we dont have one
if ( !aifl.dead && !aifl.scripted && !aifl.action && stateThread.IsIdle ( ) && aifl.scriptedEndWithIdle ) {
UpdateTactical ( 0 );
} else {
UpdateState();
}
// clear the hit enemy flag so we catch the next time we hit someone
aifl.hitEnemy = false;
if ( move.fl.allowHiddenMove || !IsHidden() ) {
// update the animstate if we're not hidden
UpdateAnimState();
}
}
/*
=====================
idAI::OnStateChange
=====================
*/
void idAI::OnStateChange ( int channel ) {
}
/*
=====================
idAI::OverrideFlag
=====================
*/
void idAI::OverrideFlag ( aiFlagOverride_t flag, bool value ) {
bool oldValue;
switch ( flag ) {
case AIFLAGOVERRIDE_DISABLEPAIN: oldValue = disablePain; break;
case AIFLAGOVERRIDE_DAMAGE: oldValue = fl.takedamage; break;
case AIFLAGOVERRIDE_NOTURN: oldValue = move.fl.noTurn; break;
case AIFLAGOVERRIDE_NOGRAVITY: oldValue = move.fl.noGravity; break;
default:
return;
}
if ( oldValue == value ) {
return;
}
int flagOverride = 1 << (flag * 2);
int flagOverrideValue = 1 << ((flag * 2) + 1);
if ( (flagOverrides & flagOverride) && !!(flagOverrides & flagOverrideValue) == value ) {
flagOverrides &= ~flagOverride;
} else {
if ( oldValue ) {
flagOverrides |= flagOverrideValue;
} else {
flagOverrides &= ~flagOverrideValue;
}
flagOverrides |= flagOverride;
}
switch ( flag ) {
case AIFLAGOVERRIDE_DISABLEPAIN: disablePain = value; break;
case AIFLAGOVERRIDE_DAMAGE: fl.takedamage = value; break;
case AIFLAGOVERRIDE_NOTURN: move.fl.noTurn = value; break;
case AIFLAGOVERRIDE_NOGRAVITY: move.fl.noGravity = value; break;
}
}
/*
=====================
idAI::RestoreFlag
=====================
*/
void idAI::RestoreFlag ( aiFlagOverride_t flag ) {
int flagOverride = 1 << (flag * 2);
int flagOverrideValue = 1 << ((flag * 2) + 1);
bool value;
if ( !(flagOverrides&flagOverride) ) {
return;
}
value = !!(flagOverrides & flagOverrideValue);
switch ( flag ) {
case AIFLAGOVERRIDE_DISABLEPAIN: disablePain = value; break;
case AIFLAGOVERRIDE_DAMAGE: fl.takedamage = value; break;
case AIFLAGOVERRIDE_NOTURN: move.fl.noTurn = value; break;
case AIFLAGOVERRIDE_NOGRAVITY: move.fl.noGravity = value; break;
}
}
/***********************************************************************
Damage
***********************************************************************/
/*
=====================
idAI::ReactionTo
=====================
*/
int idAI::ReactionTo( const idEntity *ent ) {
if ( ent->fl.hidden ) {
// ignore hidden entities
return ATTACK_IGNORE;
}
if ( !ent->IsType( idActor::GetClassType() ) ) {
return ATTACK_IGNORE;
}
if( combat.fl.ignoreEnemies ){
return ATTACK_IGNORE;
}
const idActor *actor = static_cast<const idActor *>( ent );
if ( actor->IsType( idPlayer::GetClassType() ) && static_cast<const idPlayer *>(actor)->noclip ) {
// ignore players in noclip mode
return ATTACK_IGNORE;
}
// actors on different teams will always fight each other
if ( actor->team != team ) {
if ( actor->fl.notarget ) {
// don't attack on sight when attacker is notargeted
return ATTACK_ON_DAMAGE; /* | ATTACK_ON_ACTIVATE ; */
}
return ATTACK_ON_SIGHT | ATTACK_ON_DAMAGE; /* | ATTACK_ON_ACTIVATE; */
}
//FIXME: temporarily disabled monsters of same rank fighting because it's happening too much.
// monsters will fight when attacked by lower or equal ranked monsters. rank 0 never fights back.
//if ( rank && ( actor->rank <= rank ) ) {
if ( rank && ( actor->rank < rank ) ) {
return ATTACK_ON_DAMAGE;
}
// don't fight back
return ATTACK_IGNORE;
}
/*
=====================
idAI::AdjustHealthByDamage
=====================
*/
void idAI::AdjustHealthByDamage ( int damage ) {
if ( aifl.undying ) {
return;
}
idActor::AdjustHealthByDamage ( damage );
if ( g_perfTest_aiUndying.GetBool() && health <= 0 ) {
//so we still take pain!
health = 1;
}
}
/*
=====================
idAI::Pain
=====================
*/
bool idAI::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) {
aifl.pain = idActor::Pain( inflictor, attacker, damage, dir, location );
aifl.damage = true;
// force a blink
blink_time = 0;
// No special handling if friendly fire
// jshepard: friendly fire will cause pain. Players will only be able to pain buddy marines
// via splash damage.
/*
if ( attacker && attacker->IsType ( idActor::GetClassType ( ) ) ) {
if ( static_cast<idActor*>( attacker )->team == team ) {
return aifl.pain;
}
}
*/
// ignore damage from self
if ( attacker != this ) {
// React to taking pain
ReactToPain ( attacker, damage );
pain.takenThisFrame += damage;
pain.lastTakenTime = gameLocal.time;
combat.tacticalPainTaken += damage;
// If taken too much pain where we then skip the current destination
if ( combat.tacticalPainThreshold && combat.tacticalPainTaken > combat.tacticalPainThreshold ) {
if (team==AITEAM_STROGG &&
(combat.tacticalMaskAvailable&AITACTICAL_COVER_BITS) &&
IsType(rvAITactical::GetClassType()) &&
ai_allowTacticalRush.GetBool() &&
spawnArgs.GetBool("rushOnPain", "1")) {
// clear any tether
tether = NULL;
// change ranged distances
combat.attackRange[0] = 0.0f;
combat.attackRange[1] = 100.0f; // make them get really close
// remove cover behavior
combat.tacticalMaskAvailable &= ~AITACTICAL_COVER_BITS;
// add ranged behavior
combat.tacticalMaskAvailable |= AITACTICAL_RANGED_BITS;
}
ForceTacticalUpdate ( );
return true;
}
// If looping pain and a new paintype comes in we can stop the loop
if ( pain.loopEndTime && pain.loopType.Icmp ( painType ) ) {
pain.loopEndTime = 0;
pain.loopType = painType;
}
ExecScriptFunction( funcs.damage );
}
// If we were hit by our enemy and our enemy isnt visible then
// force the enemy visiblity to be updated as if we saw him momentarily
if ( enemy.ent == attacker && !enemy.fl.visible ) {
UpdateEnemyPosition ( true );
}
return aifl.pain;
}
/*
=====================
idAI::Killed
=====================
*/
void idAI::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) {
idAngles ang;
const char* modelDeath;
const idKeyValue* kv;
if ( g_debugDamage.GetBool() ) {
gameLocal.Printf( "Damage: joint: '%s', zone '%s'\n", animator.GetJointName( ( jointHandle_t )location ),
GetDamageGroup( location ) );
}
if ( aifl.dead ) {
aifl.pain = true;
aifl.damage = true;
return;
}
aifl.dead = true;
// turn off my flashlight, if I had one
ProcessEvent( &AI_Flashlight, false );
// Detach from any spawners
if( GetSpawner() ) {
GetSpawner()->Detach( this );
SetSpawner( NULL );
}
// Hide surfaces on death
for ( kv = spawnArgs.MatchPrefix ( "deathhidesurface", NULL );
kv;
kv = spawnArgs.MatchPrefix ( "deathhidesurface", kv ) ) {
HideSurface ( kv->GetValue() );
}
// stop all voice sounds
StopSpeaking( true );
SetMoveType ( MOVETYPE_DEAD );
move.fl.noGravity = false;
move.fl.allowPushMovables = false;
aifl.scripted = false;
physicsObj.UseFlyMove( false );
physicsObj.ForceDeltaMove( false );
// end our looping ambient sound
StopSound( SND_CHANNEL_AMBIENT, false );
if ( attacker && attacker->IsType( idActor::GetClassType() ) ) {
gameLocal.AlertAI( ( idActor * )attacker );
aiManager.AnnounceKill ( this, attacker, inflictor );
aiManager.AnnounceDeath ( this, attacker );
}
if ( attacker && attacker->IsType( idActor::GetClassType() ) ) {
gameLocal.AlertAI( ( idActor * )attacker );
}
// activate targets
ActivateTargets( this );
RemoveAttachments();
RemoveProjectile();
StopMove( MOVE_STATUS_DONE );
OnDeath();
CheckDeathObjectives();
ClearEnemy();
// make monster nonsolid
physicsObj.SetContents( 0 );
physicsObj.GetClipModel()->Unlink();
Unbind();
if ( g_perfTest_aiNoRagdoll.GetBool() ) {
if ( spawnArgs.MatchPrefix( "lipsync_death" ) ) {
Speak( "lipsync_death", true );
} else {
StartSound( "snd_death", SND_CHANNEL_VOICE, 0, false, NULL );
}
physicsObj.SetLinearVelocity( vec3_zero );
physicsObj.PutToRest();
physicsObj.DisableImpact();
} else if( fl.quickBurn ){
if ( spawnArgs.MatchPrefix( "lipsync_death" ) ) {
Speak( "lipsync_death", true );
} else {
StartSound( "snd_death", SND_CHANNEL_VOICE, 0, false, NULL );
}
physicsObj.SetLinearVelocity( vec3_zero );
physicsObj.PutToRest();
physicsObj.DisableImpact();
} else {
if ( StartRagdoll() ) {
if ( spawnArgs.MatchPrefix( "lipsync_death" ) ) {
Speak( "lipsync_death", true );
} else {
StartSound( "snd_death", SND_CHANNEL_VOICE, 0, false, NULL );
}
}
if ( spawnArgs.GetString( "model_death", "", &modelDeath ) ) {
// lost soul is only case that does not use a ragdoll and has a model_death so get the death sound in here
if ( spawnArgs.MatchPrefix( "lipsync_death" ) ) {
Speak( "lipsync_death", true );
} else {
StartSound( "snd_death", SND_CHANNEL_VOICE, 0, false, NULL );
}
renderEntity.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.time );
SetModel( modelDeath );
physicsObj.SetLinearVelocity( vec3_zero );
physicsObj.PutToRest();
physicsObj.DisableImpact();
}
}
SetState ( "State_Killed" );
kv = spawnArgs.MatchPrefix( "def_drops", NULL );
while( kv ) {
idDict args;
idEntity *tEnt;
if( kv->GetValue() != "" ){
args.Set( "classname", kv->GetValue() );
args.Set( "origin", physicsObj.GetAbsBounds().GetCenter().ToString() );
// Let items know that they are of the dropped variety
args.Set( "dropped", "1" );
if (gameLocal.SpawnEntityDef( args, &tEnt )) {
if ( tEnt && tEnt->GetPhysics()) { //tEnt *should* be valid, but hey...
// magic/arbitrary number to give it some spin. Some constants used to ensure guns rarely fall standing up
tEnt->GetPhysics()->SetAngularVelocity( idVec3( (gameLocal.random.RandomFloat() * 10.0f) + 20.0f,
(gameLocal.random.RandomFloat() * 10.0f) + 20.0f,
(gameLocal.random.RandomFloat() * 10.0f) + 20.0f));
}
}
}
kv = spawnArgs.MatchPrefix( "def_drops", kv );
}
}
/***********************************************************************
Targeting/Combat
***********************************************************************/
/*
=====================
idAI::Activate
Notifies the script that a monster has been activated by a trigger or flashlight
=====================
*/
void idAI::Activate( idEntity *activator ) {
idPlayer *player;
// Set our tether?
if ( activator && activator->IsType ( rvAITether::GetClassType ( ) ) ) {
SetTether ( static_cast<rvAITether*>(activator) );
}
if ( aifl.dead ) {
// ignore it when they're dead
return;
}
// make sure he's not dormant
dormantStart = 0;
aifl.activated = true;
if ( !activator || !activator->IsType( idPlayer::GetClassType() ) ) {
player = gameLocal.GetLocalPlayer();
} else {
player = static_cast<idPlayer *>( activator );
}
if ( ReactionTo( player ) & ATTACK_ON_ACTIVATE ) {
SetEnemy( player );
}
// If being activated by a spawner we need to attach to it
if ( activator && activator->IsType ( rvSpawner::GetClassType() ) ) {
rvSpawner* spawn = static_cast<rvSpawner*>( activator );
spawn->Attach ( this );
SetSpawner ( spawn );
}
}
/*
=====================
idAI::TalkTo
=====================
*/
void idAI::TalkTo( idActor *actor ) {
// jshepard: the dead do not speak.
if ( aifl.dead )
return;
ExecScriptFunction( funcs.onclick );
// Cant talk when already talking
if ( aifl.action || IsSpeaking ( ) || !aiManager.CheckTeamTimer ( team, AITEAMTIMER_ACTION_TALK ) ) {
return;
}
switch ( GetTalkState ( ) ) {
case TALK_OK:
if ( talkMessage >= TALKMSG_LOOP ) {
//MCG: requested change: stop responding after one loop
return;
}
// at least one second between talking to people
aiManager.SetTeamTimer ( team, AITEAMTIMER_ACTION_TALK, 2000 );
talkTarget = actor;
// Move to the next talk message
talkMessage = (talkMessage_t)((int)talkMessage + 1);
// Loop until we find a valid talk message
while ( 1 ) {
idStr postfix;
if ( talkMessage >= TALKMSG_LOOP ) {
postfix = aiTalkMessageString[TALKMSG_LOOP];
postfix += va("%d", (int)(talkMessage - TALKMSG_LOOP+1) );
} else {
postfix = aiTalkMessageString[talkMessage];
}
// Try the lipsync for the specific passive prefix first
if ( Speak ( va("lipsync_%stalk_%s", passive.prefix.c_str(), postfix.c_str() ) ) ) {
break;
}
// Try the generic lipsync for the current talk message
if ( Speak ( va("lipsync_talk_%s", postfix.c_str() ) ) ) {
break;
}
// If the first loop failed then there is no other options
if ( talkMessage == TALKMSG_LOOP ) {
return;
} else if ( talkMessage >= TALKMSG_LOOP ) {
talkMessage = TALKMSG_LOOP;
} else {
talkMessage = (talkMessage_t)((int)talkMessage + 1);
}
}
// Start talking anim if we have one
if ( IsSpeaking ( ) ) {
passive.talkTime = speakTime;
}
break;
case TALK_FOLLOW:
if ( actor == leader ) {
leader = NULL;
Speak ( "lipsync_stopfollow", true );
} else {
leader = actor;
Speak ( "lipsync_follow", true );
}
break;
case TALK_BUSY:
if ( talkBusyCount > 3 )
{//only say a max of 4 lines
return;
}
talkBusyCount++;
//try to say this variant - if we can't, stop forever
if ( !Speak ( va("lipsync_busy_%d",talkBusyCount) ) )
{//nevermore
talkBusyCount = 999;
}
break;
case TALK_WAIT:
//do nothing
break;
}
}
/*
=====================
idAI::GetTalkState
=====================
*/
talkState_t idAI::GetTalkState( void ) const {
if ( ( talkState != TALK_NEVER ) && aifl.dead ) {
return TALK_DEAD;
}
if ( IsHidden() ) {
return TALK_NEVER;
}
if ( (talkState == TALK_OK || talkState == TALK_FOLLOW) && aifl.scripted ) {
return TALK_BUSY;
}
return talkState;
}
/*
=====================
idAI::TouchedByFlashlight
=====================
*/
void idAI::TouchedByFlashlight( idActor *flashlight_owner ) {
if ( RespondToFlashlight() ) {
Activate( flashlight_owner );
}
}
/*
=====================
idAI::ClearEnemy
=====================
*/
void idAI::ClearEnemy( bool dead ) {
// Dont bother if we dont have an enemy
if ( !enemy.ent ) {
return;
}
if ( move.moveCommand == MOVE_TO_ENEMY ) {
StopMove( MOVE_STATUS_DEST_NOT_FOUND );
} else if ( !aifl.scripted
&& move.moveCommand == MOVE_TO_COVER
&& !move.fl.done
&& aasSensor->Reserved()
&& !IsBehindCover() ) {
//clear cover and stop move
aasSensor->Reserve( NULL );
combat.coverValidTime = 0;
OnCoverInvalidated();
ForceTacticalUpdate();
StopMove( MOVE_STATUS_DONE );
}
enemyNode.Remove();
enemy.ent = NULL;
enemy.fl.dead = dead;
combat.fl.seenEnemyDirectly = false;
UpdateHelper ( );
}
/*
=====================
idAI::UpdateEnemyPosition
=====================
*/
void idAI::UpdateEnemyPosition ( bool forceUpdate ) {
if( !enemy.ent || (!enemy.fl.visible && !forceUpdate) ) {
return;
}
idActor* enemyActor = dynamic_cast<idActor*>(enemy.ent.GetEntity());
idEntity* enemyEnt = static_cast<idEntity*>(enemy.ent.GetEntity());
enemy.lastVisibleFromEyePosition = GetEyePosition ( );
// Update the enemy origin if it isnt locked
if ( !enemy.fl.lockOrigin ) {
enemy.lastKnownPosition = enemyEnt->GetPhysics()->GetOrigin();
if ( enemyActor ) {
enemy.lastVisibleEyePosition = enemyActor->GetEyePosition();
enemy.lastVisibleChestPosition = enemyActor->GetChestPosition();
} else {
enemy.lastVisibleEyePosition = enemy.ent->GetPhysics()->GetOrigin ( );
enemy.lastVisibleChestPosition = enemy.ent->GetPhysics()->GetOrigin ( );
}
}
}
/*
=====================
idAI::UpdateEnemy
=====================
*/
void idAI::UpdateEnemy ( void ) {
predictedPath_t predictedPath;
// If we lost our enemy then clear it out to be sure
if( !enemy.ent ) {
return;
}
// Rest to not being in fov
enemy.fl.inFov = false;
// Bail out if we arent queued to do a complex think
if ( aifl.simpleThink ) {
// Keep the fov flag up to date
if ( IsEnemyVisible ( ) ) {
enemy.fl.inFov = CheckFOV( enemy.lastKnownPosition );
}
} else {
bool oldVisible;
// Cache current enemy visiblity so we can see if it changes
oldVisible = IsEnemyVisible ( );
// See if enemy still visible
UpdateEnemyVisibility ( );
// If our enemy isnt visible but is within aware range then we know where he is
if ( !IsEnemyVisible ( ) && DistanceTo ( enemy.ent ) < combat.awareRange ) {
UpdateEnemyPosition( true );
} else {
/*
// check if we heard any sounds in the last frame
if ( enemyEnt == gameLocal.GetAlertActor() ) {
float dist = ( enemyEnt->GetPhysics()->GetOrigin() - physicsObj.GetOrigin() ).LengthSqr();
if ( dist < Square( combat.earRange ) ) {
SetEnemyPosition();
}
}
*/
}
// Update fov
enemy.fl.inFov = CheckFOV( enemy.lastKnownPosition );
// Set enemy.visibleTime to the time when the visibility flag changed
if ( oldVisible != IsEnemyVisible ( ) ) {
// Just caught sight of the enemy after loosing him, delay the attack actions a bit
if ( !oldVisible && combat.attackSightDelay ) {
float delay;
if ( enemy.lastVisibleChangeTime ) {
// The longer the enemy has not been visible, the more we should delay
delay = idMath::ClampFloat ( 0.0f, 1.0f, (gameLocal.time - enemy.lastVisibleChangeTime) / AI_SIGHTDELAYSCALE );
} else {
// The enemy has never been visible so delay the full amount
delay = 1.0f;
}
// Add to the ranged attack timer providing its not already running a timer
if ( actionTimerRangedAttack.IsDone ( actionTime) ) {
actionTimerRangedAttack.Clear ( actionTime );
actionTimerRangedAttack.Add ( combat.attackSightDelay * delay, 0.5f );
}
// Add to the special attack timer providing its not already running a timer
if ( actionTimerSpecialAttack.IsDone ( actionTime ) ) {
actionTimerSpecialAttack.Clear ( actionTime );
actionTimerSpecialAttack.Add ( combat.attackSightDelay * delay, 0.5f );
}
}
enemy.lastVisibleChangeTime = gameLocal.time;
}
// Handler for visibility change
if ( oldVisible != IsEnemyVisible ( ) ) {
OnEnemyVisiblityChange ( oldVisible );
}
}
// Adjust smoothed linear and pushed velocities for enemies
if ( enemy.fl.visible ) {
enemy.smoothedLinearVelocity += ((enemy.ent->GetPhysics()->GetLinearVelocity ( ) - enemy.ent->GetPhysics()->GetPushedLinearVelocity ( ) - enemy.smoothedLinearVelocity) * enemy.smoothVelocityRate );
enemy.smoothedPushedVelocity += ((enemy.ent->GetPhysics()->GetPushedLinearVelocity ( ) - enemy.smoothedPushedVelocity) * enemy.smoothVelocityRate );
} else {
enemy.smoothedLinearVelocity -= (enemy.smoothedLinearVelocity * enemy.smoothVelocityRate );
enemy.smoothedPushedVelocity -= (enemy.smoothedPushedVelocity * enemy.smoothVelocityRate );
}
// Update enemy range
enemy.range = DistanceTo ( enemy.lastKnownPosition );
enemy.range2d = DistanceTo2d ( enemy.lastKnownPosition );
// Calulcate the aggression scale
// Skill level 0: (1.0)
// Skill level 1: (1.0 - 1.25)
// Skill level 2: (1.0 - 1.5)
// Skill level 3: (1.0 - 1.75)
if ( combat.aggressiveRange > 0.0f ) {
combat.aggressiveScale = (g_skill.GetFloat() / MAX_SKILL_LEVELS);
combat.aggressiveScale *= 1.0f - idMath::ClampFloat ( 0.0f, 1.0f, enemy.range / combat.aggressiveRange );
combat.aggressiveScale += 1.0f;
}
}
/*
=====================
idAI::LastKnownPosition
=====================
*/
const idVec3& idAI::LastKnownPosition ( const idEntity *ent ) {
return (ent==enemy.ent)?(enemy.lastKnownPosition):(ent->GetPhysics()->GetOrigin());
}
/*
=====================
idAI::UpdateEnemyVisibility
Update whether or not the AI can see its enemy or not and determine how.
=====================
*/
void idAI::UpdateEnemyVisibility ( void ) {
enemy.fl.visible = false;
// No enemy so nothing to see
if ( !enemy.ent ) {
return;
}
// Update enemy visibility flag
enemy.fl.visible = CanSeeFrom ( GetEyePosition ( ), enemy.ent, false );
// IF the enemy isnt visible and not forcing an update we can just early out
if ( !enemy.fl.visible ) {
return;
}
// If we are seeing our enemy for the first time after changing enemies, call him out
if ( enemy.lastVisibleTime < enemy.changeTime ) {
AnnounceNewEnemy( );
}
combat.fl.seenEnemyDirectly = true;
enemy.lastVisibleTime = gameLocal.time;
// Update our known locations of the enemy since we just saw him
UpdateEnemyPosition ( );
}
/*
=====================
idAI::SetSpawner
=====================
*/
void idAI::SetSpawner ( rvSpawner* _spawner ) {
spawner = _spawner;
}
/*
=====================
idAI::SetSpawner
=====================
*/
rvSpawner* idAI::GetSpawner ( void ) {
return spawner;
}
/*
=====================
idAI::SetEnemy
=====================
*/
bool idAI::SetEnemy( idEntity *newEnemy ) {
idEntity* oldEnemy;
// Look for obvious early out
if ( enemy.ent == newEnemy ) {
return true;
}
// Cant set enemy if dead
if ( aifl.dead ) {
ClearEnemy ( false );
return false;
}
// Cant set enemy if the enemy is dead or has no target set
if ( newEnemy ) {
if ( newEnemy->health <= 0 || newEnemy->fl.notarget ) {
return false;
}
}
oldEnemy = enemy.ent;
enemy.fl.dead = false;
enemy.fl.lockOrigin = false;
if ( !newEnemy ) {
ClearEnemy ( false );
} else {
// Set our current enemy
enemy.ent = newEnemy;
// If the enemy is an actor then add to the actors enemy list
if( newEnemy->IsType( idActor::GetClassType() ) ){
idActor* enemyActor = static_cast<idActor*>( newEnemy );
enemyNode.AddToEnd( enemyActor->enemyList );
}
}
// Allow custom handling of enemy change
if ( enemy.ent != oldEnemy ) {
OnEnemyChange ( oldEnemy );
}
return true;
}
/*
===================
idAI::CalculateAttackOffsets
calculate joint positions on attack frames so we can do proper "can hit" tests
===================
*/
void idAI::CalculateAttackOffsets ( void ) {
const idDeclModelDef* modelDef;
int num;
int i;
int frame;
const frameCommand_t* command;
idMat3 axis;
const idAnim* anim;
jointHandle_t joint;
modelDef = animator.ModelDef();
if ( !modelDef ) {
return;
}
num = modelDef->NumAnims();
// needs to be off while getting the offsets so that we account for the distance the monster moves in the attack anim
animator.RemoveOriginOffset( false );
// anim number 0 is reserved for non-existant anims. to avoid off by one issues, just allocate an extra spot for
// launch offsets so that anim number can be used without subtracting 1.
attackAnimInfo.SetGranularity( 1 );
attackAnimInfo.SetNum( num + 1 );
attackAnimInfo[ 0 ].attackOffset.Zero();
attackAnimInfo[ 0 ].eyeOffset.Zero();
for( i = 1; i <= num; i++ ) {
attackAnimInfo[ i ].attackOffset.Zero();
attackAnimInfo[ i ].eyeOffset.Zero();
anim = modelDef->GetAnim( i );
if ( !anim ) {
continue;
}
frame = anim->FindFrameForFrameCommand( FC_AI_ATTACK, &command );
if ( frame >= 0 ) {
joint = animator.GetJointHandle( command->joint->c_str() );
if ( joint == INVALID_JOINT ) {
gameLocal.Error( "Invalid joint '%s' on 'ai_attack' frame command on frame %d of model '%s'", command->joint->c_str(), frame, modelDef->GetName() );
}
GetJointTransformForAnim( joint, i, FRAME2MS( frame ), attackAnimInfo[ i ].attackOffset, axis );
if ( chestOffsetJoint!= INVALID_JOINT ) {
GetJointTransformForAnim( chestOffsetJoint, i, FRAME2MS( frame ), attackAnimInfo[ i ].eyeOffset, axis );
}
}
}
animator.RemoveOriginOffset( true );
}
/*
=====================
idAI::CreateProjectileClipModel
=====================
*/
void idAI::CreateProjectileClipModel( void ) const {
if ( projectileClipModel == NULL ) {
idBounds projectileBounds( vec3_origin );
projectileBounds.ExpandSelf( 2.0f ); // projectileRadius );
projectileClipModel = new idClipModel( idTraceModel( projectileBounds ) );
}
}
/*
=====================
idAI::GetPredictedAimDirOffset
=====================
*/
void idAI::GetPredictedAimDirOffset ( const idVec3& source, const idVec3& target, float projectileSpeed, const idVec3& targetVelocity, idVec3& offset ) const {
float a;
float b;
float c;
float d;
float t;
idVec3 r;
// Make sure there is something to predict
if ( targetVelocity.LengthSqr ( ) == 0.0f ) {
offset.Zero ( );
return;
}
// Solve for (t): magnitude(targetVelocity * t + target - source) = projectileSpeed * t
r = (target-source);
a = (targetVelocity * targetVelocity) - Square(projectileSpeed);
b = (targetVelocity * 2.0f) * r;
c = r*r;
d = b*b - 4*a*c;
t = 0.0f;
if ( d >= 0.0f ) {
float t1;
float t2;
float denom;
d = idMath::Sqrt(d);
denom = 1.0f / (2.0f * a);
t1 = (-b + d) * denom;
t2 = (-b - d) * denom;
if ( t1 < 0.0f && t2 < 0.0f ) {
t = 0.0f;
} else if ( t1 < 0.0f ) {
t = t2;
} else if ( t2 < 0.0f ) {
t = t1;
} else {
t = Min(t1,t2);
}
}
offset = targetVelocity * t;
}
/*
=====================
idAI::GetAimDir
=====================
*/
bool idAI::GetAimDir(
const idVec3& firePos,
const idEntity* target,
const idDict* projectileDef,
idEntity* ignore,
idVec3& aimDir,
float aimOffset,
float predict
) const
{
idVec3 targetPos1;
idVec3 targetPos2;
idVec3 targetLinearVel;
idVec3 targetPushedVel;
idVec3 delta;
float max_height;
bool result;
idEntity* targetEnt = const_cast<idEntity*>(target);
// if no aimAtEnt or projectile set
if ( !targetEnt ) {
aimDir = viewAxis[ 0 ] * physicsObj.GetGravityAxis();
return false;
}
float projectileSpeed = idProjectile::GetVelocity ( projectileDef ).LengthFast ( );
idVec3 projectileGravity = idProjectile::GetGravity ( projectileDef );
if ( projectileClipModel == NULL ) {
CreateProjectileClipModel();
}
if ( targetEnt == enemy.ent ) {
targetPos2 = enemy.lastVisibleEyePosition;
targetPos1 = enemy.lastVisibleChestPosition;
targetPushedVel = enemy.smoothedPushedVelocity;
targetLinearVel = enemy.smoothedLinearVelocity;
} else if ( targetEnt->IsType( idActor::GetClassType() ) ) {
targetPos2 = static_cast<idActor*>(targetEnt)->GetEyePosition ( );
targetPos1 = static_cast<idActor*>(targetEnt)->GetChestPosition ( );
targetPushedVel = target->GetPhysics()->GetPushedLinearVelocity ( );
targetLinearVel = (target->GetPhysics()->GetLinearVelocity ( ) - targetPushedVel);
} else {
targetPos1 = targetEnt->GetPhysics()->GetAbsBounds().GetCenter();
targetPos2 = targetPos1;
targetPushedVel.Zero ( );
targetLinearVel.Zero ( );
}
// Target prediction
if ( projectileSpeed == 0.0f ) {
// Hitscan prediction actually causes the hitscan to miss unless it is predicted.
delta = (targetLinearVel * (-1.0f + predict));
} else {
// Projectile prediction must figure out how far to lead the shot to hit the target
GetPredictedAimDirOffset ( firePos, targetPos1, projectileSpeed, (targetLinearVel*predict)+targetPushedVel, delta );
}
targetPos1 += delta;
targetPos2 += delta;
result = false;
// try aiming for chest
delta = targetPos1 - firePos;
max_height = (delta.NormalizeFast ( ) + aimOffset) * projectile_height_to_distance_ratio;
if ( max_height > 0.0f ) {
result = PredictTrajectory( firePos, targetPos1 + delta * aimOffset, projectileSpeed, projectileGravity, projectileClipModel, MASK_SHOT_RENDERMODEL, max_height, ignore, targetEnt, ai_debugTrajectory.GetBool() ? 1000 : 0, aimDir );
if ( result || !targetEnt->IsType( idActor::GetClassType() ) ) {
return result;
}
}
// try aiming for head
delta = targetPos2 - firePos;
max_height = (delta.NormalizeFast ( ) + aimOffset) * projectile_height_to_distance_ratio;
if ( max_height > 0.0f ) {
result = PredictTrajectory( firePos, targetPos2 + delta * aimOffset, projectileSpeed, projectileGravity, projectileClipModel, MASK_SHOT_RENDERMODEL, max_height, ignore, targetEnt, ai_debugTrajectory.GetBool() ? 1000 : 0, aimDir );
}
return result;
}
/*
=====================
idAI::CreateProjectile
=====================
*/
idProjectile *idAI::CreateProjectile ( const idDict* projectileDict, const idVec3 &pos, const idVec3 &dir ) {
idEntity* ent;
const char* clsname;
if ( !projectile.GetEntity() ) {
gameLocal.SpawnEntityDef( *projectileDict, &ent, false );
if ( !ent ) {
clsname = projectileDict->GetString( "classname" );
gameLocal.Error( "Could not spawn entityDef '%s'", clsname );
}
if ( !ent->IsType( idProjectile::GetClassType() ) ) {
clsname = ent->GetClassname();
gameLocal.Error( "'%s' is not an idProjectile", clsname );
}
projectile = (idProjectile*)ent;
}
projectile.GetEntity()->Create( this, pos, dir );
return projectile.GetEntity();
}
/*
=====================
idAI::RemoveProjectile
=====================
*/
void idAI::RemoveProjectile( void ) {
if ( projectile.GetEntity() ) {
projectile.GetEntity()->PostEventMS( &EV_Remove, 0 );
projectile = NULL;
}
}
/*
=====================
idAI::Attack
=====================
*/
bool idAI::Attack ( const char* attackName, jointHandle_t joint, idEntity* target, const idVec3& pushVelocity ) {
// Get the attack dictionary
const idDict* attackDict;
attackDict = gameLocal.FindEntityDefDict ( spawnArgs.GetString ( va("def_attack_%s", attackName ) ), false );
if ( !attackDict ) {
gameLocal.Error ( "could not find attack entityDef 'def_attack_%s (%s)' on AI entity %s", attackName, spawnArgs.GetString ( va("def_attack_%s", attackName ) ), GetName ( ) );
}
// Melee Attack?
if ( spawnArgs.GetBool ( va("attack_%s_melee", attackName ), "0" ) ) {
return AttackMelee ( attackName, attackDict );
}
// Ranged attack (hitscan or projectile)?
return ( AttackRanged ( attackName, attackDict, joint, target, pushVelocity ) != NULL );
}
/*
=====================
idAI::AttackRanged
=====================
*/
idProjectile* idAI::AttackRanged (
const char* attackName,
const idDict* attackDict,
jointHandle_t joint,
idEntity* target,
const idVec3& pushVelocity
)
{
float attack_accuracy;
float attack_cone;
float attack_spread;
int attack_count;
bool attack_hitscan;
float attack_predict;
float attack_pullback;
int i;
idVec3 muzzleOrigin;
idMat3 muzzleAxis;
idVec3 dir;
idAngles ang;
idMat3 axis;
idProjectile* lastProjectile;
lastProjectile = NULL;
// Generic attack properties
attack_accuracy = spawnArgs.GetFloat ( va("attack_%s_accuracy", attackName ), "7" );
attack_cone = spawnArgs.GetFloat ( va("attack_%s_cone", attackName ), "75" );
attack_spread = DEG2RAD ( spawnArgs.GetFloat ( va("attack_%s_spread", attackName ), "0" ) );
attack_count = spawnArgs.GetInt ( va("attack_%s_count", attackName ), "1" );
attack_hitscan = spawnArgs.GetBool ( va("attack_%s_hitscan", attackName ), "0" );
attack_predict = spawnArgs.GetFloat ( va("attack_%s_predict", attackName ), "0" );
attack_pullback = spawnArgs.GetFloat ( va("attack_%s_pullback", attackName ), "0" );
// Get the muzzle origin and axis from the given launch joint
GetMuzzle( joint, muzzleOrigin, muzzleAxis );
if ( attack_pullback )
{
muzzleOrigin -= muzzleAxis[0]*attack_pullback;
}
// set aiming direction
bool calcAim = true;
if ( GetEnemy() && GetEnemy() == target && GetEnemy() == gameLocal.GetLocalPlayer() && spawnArgs.GetBool( va("attack_%s_missFirstShot",attackName) ) ) {
//purposely miss
if ( gameLocal.random.RandomFloat() < 0.5f ) {
//actually, hit anyway...
} else {
idVec3 targetPos = enemy.lastVisibleEyePosition;
idVec3 eFacing;
idVec3 left, up;
up.Set( 0, 0, 1 );
left = viewAxis[0].Cross(up);
if ( GetEnemy()->IsType( idActor::GetClassType() ) )
{
eFacing = ((idActor*)GetEnemy())->viewAxis[0];
}
else
{
eFacing = GetEnemy()->GetPhysics()->GetAxis()[0];
}
if ( left*eFacing > 0 )
{
targetPos += left * ((gameLocal.random.RandomFloat()*8.0f) + 8.0f);
}
else
{
targetPos -= left * ((gameLocal.random.RandomFloat()*8.0f) + 8.0f);
}
dir = targetPos-muzzleOrigin;
dir.Normalize();
attack_accuracy = 0;
calcAim = false;
//don't miss next time
spawnArgs.SetBool( va("attack_%s_missFirstShot",attackName), false );
}
}
if ( calcAim ) {
if ( target && !spawnArgs.GetBool ( va("attack_%s_lockToJoint",attackName), "0" ) ) {
GetAimDir( muzzleOrigin, target, attackDict, this, dir,
spawnArgs.GetFloat ( va("attack_%s_aimoffset", attackName )),
attack_predict );
} else {
dir = muzzleAxis[0];
if ( spawnArgs.GetBool ( va("attack_%s_lockToJoint",attackName), "0" ) )
{
if ( spawnArgs.GetBool( va("attack_%s_traceToNextJoint", attackName ), "0" ) )
{
jointHandle_t endJoint = animator.GetFirstChild( joint );
if ( endJoint != INVALID_JOINT && endJoint != joint )
{
idVec3 endJointPos;
idMat3 blah;
GetJointWorldTransform( endJoint, gameLocal.GetTime(), endJointPos, blah );
dir = endJointPos-muzzleOrigin;
//ARGHH: need to be able to set range!!!
//const_cast<idDict*>(attackDict)->SetFloat( "range", dir.Normalize() );//NOTE: yes, I intentionally normalize here as well as use the result for length...
dir.Normalize();
}
}
}
}
}
// random accuracy
ang = dir.ToAngles();
ang.pitch += gameLocal.random.CRandomFloat ( ) * attack_accuracy;
ang.yaw += gameLocal.random.CRandomFloat ( ) * attack_accuracy;
// Lock attacks to a cone?
if ( attack_cone ) {
float diff;
// clamp the attack direction to be within monster's attack cone so he doesn't do
// things like throw the missile backwards if you're behind him
diff = idMath::AngleDelta( ang.yaw, move.current_yaw );
if ( diff > attack_cone ) {
ang.yaw = move.current_yaw + attack_cone;
} else if ( diff < -attack_cone ) {
ang.yaw = move.current_yaw - attack_cone;
}
}
ang.Normalize360 ( );
axis = ang.ToMat3();
for( i = 0; i < attack_count; i++ ) {
float angle;
float spin;
// spread the projectiles out
angle = idMath::Sin( attack_spread * gameLocal.random.RandomFloat() );
spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat();
dir = axis[ 0 ] + axis[ 2 ] * ( angle * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) );
dir.Normalize();
if ( attack_hitscan ) {
gameLocal.HitScan( *attackDict, muzzleOrigin, dir, muzzleOrigin, this, false, combat.aggressiveScale );
} else {
// launch the projectile
if ( !projectile.GetEntity() ) {
CreateProjectile( attackDict, muzzleOrigin, dir );
}
lastProjectile = projectile.GetEntity();
lastProjectile->Launch( muzzleOrigin, dir, pushVelocity, 0.0f, combat.aggressiveScale );
// Let the script manage projectiles if need be
ExecScriptFunction ( funcs.launch_projectile, lastProjectile );
projectile = NULL;
}
}
lastAttackTime = gameLocal.time;
// If shooting at another ai entity then kick off an attack reaction
if ( enemy.ent && enemy.ent->IsType ( idAI::GetClassType() ) ) {
static_cast<idAI*>(enemy.ent.GetEntity())->ReactToShotAt ( this, muzzleOrigin, axis[0] );
}
return lastProjectile;
}
/*s
================
idAI::DamageFeedback
callback function for when another entity recieved damage from this entity
================
*/
void idAI::DamageFeedback( idEntity *victim, idEntity *inflictor, int &damage ) {
if ( ( victim == this ) && inflictor->IsType( idProjectile::GetClassType() ) ) {
// monsters only get half damage from their own projectiles
damage = ( damage + 1 ) / 2; // round up so we don't do 0 damage
} else if ( victim == enemy.ent ) {
aifl.hitEnemy = true;
}
}
/*
=====================
idAI::DirectDamage
Causes direct damage to an entity
kickDir is specified in the monster's coordinate system, and gives the direction
that the view kick and knockback should go
=====================
*/
void idAI::DirectDamage( const char *meleeDefName, idEntity *ent ) {
const idDict *meleeDef;
const char *p;
const idSoundShader *shader;
meleeDef = gameLocal.FindEntityDefDict( meleeDefName, false );
if ( !meleeDef ) {
gameLocal.Error( "Unknown damage def '%s' on '%s'", meleeDefName, name.c_str() );
}
if ( !ent->fl.takedamage ) {
const idSoundShader *shader = declManager->FindSound(meleeDef->GetString( "snd_miss" ));
StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL );
return;
}
//
// do the damage
//
p = meleeDef->GetString( "snd_hit" );
if ( p && *p ) {
shader = declManager->FindSound( p );
StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL );
}
idVec3 kickDir;
meleeDef->GetVector( "kickDir", "0 0 0", kickDir );
idVec3 globalKickDir;
globalKickDir = ( viewAxis * physicsObj.GetGravityAxis() ) * kickDir;
// ent->Damage( this, this, globalKickDir, meleeDefName, 1.0f, INVALID_JOINT );
float damageScale = spawnArgs.GetFloat( "damageScale", "1" );
ent->Damage( this, this, globalKickDir, meleeDefName, damageScale, NULL );
}
/*
=====================
idAI::TestMelee
=====================
*/
bool idAI::TestMelee( void ) const {
trace_t trace;
idEntity* enemyEnt = enemy.ent;
if ( !enemyEnt || !combat.meleeRange ) {
return false;
}
//FIXME: make work with gravity vector
idVec3 org = physicsObj.GetOrigin();
const idBounds &myBounds = physicsObj.GetBounds();
idBounds bounds;
// expand the bounds out by our melee range
bounds[0][0] = -combat.meleeRange;
bounds[0][1] = -combat.meleeRange;
bounds[0][2] = myBounds[0][2] - 4.0f;
bounds[1][0] = combat.meleeRange;
bounds[1][1] = combat.meleeRange;
bounds[1][2] = myBounds[1][2] + 4.0f;
bounds.TranslateSelf( org );
idVec3 enemyOrg = enemyEnt->GetPhysics()->GetOrigin();
idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds();
enemyBounds.TranslateSelf( enemyOrg );
if ( DebugFilter(ai_debugMove) ) { //YELLOW = Test Melee Bounds
gameRenderWorld->DebugBounds( colorYellow, bounds, vec3_zero, gameLocal.msec );
}
if ( !bounds.IntersectsBounds( enemyBounds ) ) {
return false;
}
idVec3 start = GetEyePosition();
idVec3 end = enemyEnt->GetEyePosition();
gameLocal.TracePoint( this, trace, start, end, MASK_SHOT_BOUNDINGBOX, this );
if ( ( trace.fraction == 1.0f ) || ( gameLocal.GetTraceEntity( trace ) == enemyEnt ) ) {
return true;
}
return false;
}
/*
=====================
idAI::AttackMelee
jointname allows the endpoint to be exactly specified in the model,
as for the commando tentacle. If not specified, it will be set to
the facing direction + combat.meleeRange.
kickDir is specified in the monster's coordinate system, and gives the direction
that the view kick and knockback should go
=====================
*/
bool idAI::AttackMelee ( const char *attackName, const idDict* meleeDict ) {
idEntity* enemyEnt = enemy.ent;
const char* p;
const idSoundShader* shader;
if ( !enemyEnt ) {
p = meleeDict->GetString( "snd_miss" );
if ( p && *p ) {
shader = declManager->FindSound( p );
StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL );
}
return false;
}
// check for the "saving throw" automatic melee miss on lethal blow
// stupid place for this.
bool forceMiss = false;
if ( enemyEnt->IsType( idPlayer::GetClassType() ) && g_skill.GetInteger() < 2 ) {
int damage, armor;
idPlayer *player = static_cast<idPlayer*>( enemyEnt );
player->CalcDamagePoints( this, this, meleeDict, 1.0f, INVALID_JOINT, &damage, &armor );
if ( enemyEnt->health <= damage ) {
int t = gameLocal.time - player->lastSavingThrowTime;
if ( t > SAVING_THROW_TIME ) {
player->lastSavingThrowTime = gameLocal.time;
t = 0;
}
if ( t < 1000 ) {
forceMiss = true;
}
}
}
// make sure the trace can actually hit the enemy
if ( forceMiss || !TestMelee( ) ) {
// missed
p = meleeDict->GetString( "snd_miss" );
if ( p && *p ) {
shader = declManager->FindSound( p );
StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL );
}
return false;
}
//
// do the damage
//
p = meleeDict->GetString( "snd_hit" );
if ( p && *p ) {
shader = declManager->FindSound( p );
StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL );
}
idVec3 kickDir;
meleeDict->GetVector( "kickDir", "0 0 0", kickDir );
idVec3 globalKickDir;
globalKickDir = ( viewAxis * physicsObj.GetGravityAxis() ) * kickDir;
// This allows some AI to be soft against melee-- most marines are selfMeleeDamageScale of 3, which means they take 3X from melee attacks!
float damageScale = spawnArgs.GetFloat( "damageScale", "1" ) * enemyEnt->spawnArgs.GetFloat ( "selfMeleeDamageScale", "1" );
//if attacker is a melee superhero, damageScale is way increased
idAI* enemyAI = static_cast<idAI*>(enemyEnt);
if( aifl.meleeSuperhero) {
if( damageScale >=1 ) {
damageScale *= 6;
} else {
damageScale = 6;
}
}
//if the defender is a melee superhero, damageScale is way decreased
if( enemyAI->aifl.meleeSuperhero) {
damageScale = 0.5f;
}
int location = INVALID_JOINT;
if ( enemyEnt->IsType ( idAI::Type ) ) {
location = static_cast<idAI*>(enemyEnt)->chestOffsetJoint;
}
enemyEnt->Damage( this, this, globalKickDir, meleeDict->GetString ( "classname" ), damageScale, location );
if ( meleeDict->GetString( "fx_impact", NULL ) ) {
if ( enemyEnt == gameLocal.GetLocalPlayer() ) {
idPlayer *ePlayer = static_cast<idPlayer*>(enemyEnt);
if ( ePlayer ) {
idVec3 dir = ePlayer->firstPersonViewOrigin-GetEyePosition();
dir.Normalize();
idVec3 org = ePlayer->firstPersonViewOrigin + (dir * -((gameLocal.random.RandomFloat()*8.0f)+8.0f) );
idAngles ang = ePlayer->firstPersonViewAxis.ToAngles() * -1;
idMat3 axis = ang.ToMat3();
gameLocal.PlayEffect( *meleeDict, "fx_impact", org, viewAxis );
}
}
}
lastAttackTime = gameLocal.time;
return true;
}
/*
================
idAI::PushWithAF
================
*/
void idAI::PushWithAF( void ) {
int i, j;
afTouch_t touchList[ MAX_GENTITIES ];
idEntity *pushed_ents[ MAX_GENTITIES ];
idEntity *ent;
idVec3 vel;
int num_pushed;
num_pushed = 0;
af.ChangePose( this, gameLocal.time );
int num = af.EntitiesTouchingAF( touchList );
for( i = 0; i < num; i++ ) {
if ( touchList[ i ].touchedEnt->IsType( idProjectile::GetClassType() ) ) {
// skip projectiles
continue;
}
// make sure we havent pushed this entity already. this avoids causing double damage
for( j = 0; j < num_pushed; j++ ) {
if ( pushed_ents[ j ] == touchList[ i ].touchedEnt ) {
break;
}
}
if ( j >= num_pushed ) {
ent = touchList[ i ].touchedEnt;
pushed_ents[num_pushed++] = ent;
vel = ent->GetPhysics()->GetAbsBounds().GetCenter() - touchList[ i ].touchedByBody->GetWorldOrigin();
vel.Normalize();
ent->GetPhysics()->SetLinearVelocity( 100.0f * vel, touchList[ i ].touchedClipModel->GetId() );
}
}
}
/***********************************************************************
Misc
***********************************************************************/
/*
================
idAI::GetMuzzle
================
*/
void idAI::GetMuzzle( jointHandle_t joint, idVec3 &muzzle, idMat3 &axis ) {
if ( joint == INVALID_JOINT ) {
muzzle = physicsObj.GetOrigin() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 14;
muzzle -= physicsObj.GetGravityNormal() * physicsObj.GetBounds()[ 1 ].z * 0.5f;
} else {
GetJointWorldTransform( joint, gameLocal.time, muzzle, axis );
//MCG
//pull the muzzle back inside the bounds if possible
//FIXME: this is nasty, we should just be able to check for starting in solid and register that as a hit! But...
/*
float scale = 0.0f;
if ( physicsObj.GetBounds().RayIntersection( muzzle, -axis[0], scale ) )
{
if ( scale != 0.0f )
{//not already inside
muzzle += scale * -axis[0];
}
}
else
{//just pull it back a little anyway?
idVec3 xyOfs = muzzle-physicsObj.GetOrigin();
xyOfs.z = 0;
muzzle += xyOfs.Length() * -axis[0];
}
*/
}
}
/*
================
idAI::Hide
================
*/
void idAI::Hide( void ) {
idActor::Hide();
fl.takedamage = false;
physicsObj.SetContents( 0 );
physicsObj.GetClipModel()->Unlink();
StopSound( SND_CHANNEL_AMBIENT, false );
enemy.fl.inFov = false;
enemy.fl.visible = false;
StopMove( MOVE_STATUS_DONE );
}
/*
================
idAI::Show
================
*/
void idAI::Show( void ) {
idActor::Show();
if ( spawnArgs.GetBool( "big_monster" ) ) {
physicsObj.SetContents( 0 );
} else if ( use_combat_bbox ) {
physicsObj.SetContents( CONTENTS_BODY|CONTENTS_SOLID );
} else {
physicsObj.SetContents( CONTENTS_BODY );
}
physicsObj.GetClipModel()->Link();
fl.takedamage = !spawnArgs.GetBool( "noDamage" );
StartSound( "snd_ambient", SND_CHANNEL_AMBIENT, 0, false, NULL );
}
/*
================
idAI::CanPlayChatterSounds
Used for playing chatter sounds on monsters.
================
*/
bool idAI::CanPlayChatterSounds( void ) const {
if ( aifl.dead ) {
return false;
}
if ( IsHidden() ) {
return false;
}
if ( enemy.ent ) {
return true;
}
if ( spawnArgs.GetBool( "no_idle_chatter" ) ) {
return false;
}
return true;
}
/*
=====================
idAI::UpdateChatter
=====================
*/
void idAI::UpdateChatter ( void ) {
int chatterRate;
const char* chatter;
// No chatter?
if ( !chatterRateIdle && !chatterRateCombat ) {
return;
}
// check if it's time to play a chat sound
if ( IsHidden() || !aifl.awake || aifl.dead || ( chatterTime > gameLocal.time ) ) {
return;
}
// Skip first chatter
if ( enemy.ent ) {
chatter = "lipsync_chatter_combat";
chatterRate = chatterRateCombat;
} else {
chatter = "lipsync_chatter_idle";
chatterRate = chatterRateIdle;
}
// Can chatter ?
if ( !chatterRate ) {
return;
}
// Start chattering, but not if he's already speaking. And he might already be speaking because he was scripted to do so.
if ( chatterTime > 0 && !IsSpeaking() ) {
Speak ( chatter, true );
}
// set the next chat time
chatterTime = gameLocal.time + chatterRate + (gameLocal.random.RandomFloat() * chatterRate * 0.5f) - (chatterRate * 0.25f);
}
/*
=====================
idAI::HeardSound
=====================
*/
idEntity *idAI::HeardSound( int ignore_team ){
// check if we heard any sounds in the last frame
idActor *actor = gameLocal.GetAlertActor();
if ( actor && ( !ignore_team || ( ReactionTo( actor ) & ATTACK_ON_SIGHT ) ) && gameLocal.InPlayerPVS( this ) ) {
idVec3 pos = actor->GetPhysics()->GetOrigin();
idVec3 org = physicsObj.GetOrigin();
float dist = ( pos - org ).LengthSqr();
if ( dist < Square( combat.earRange ) ) {
//really close?
if ( dist < Square( combat.earRange/4.0f ) ) {
return actor;
//possible LOS
} else if ( dist < Square( combat.visRange * 2.0f ) && CanSee( actor, false ) ) {
return actor;
} else if ( combat.fl.aware ) {
//FIXME: or, maybe find cover/hide/ambush spot and wait to ambush them?
//don't have an enemy and not tethered to a position
} else if ( !GetEnemy() && !tether ) {
//go into search mode
WanderAround();
move.fl.noRun = false;
move.fl.idealRunning = true;
//undid this: was causing them to not be able to do fine nav.
//move.fl.noWalk = true;
}
}
}
return NULL;
}
/*
============
idAI::SetLeader
============
*/
void idAI::SetLeader ( idEntity *newLeader ) {
idEntity* oldLeader = leader;
if( !newLeader ){
leader = NULL;
} else if ( !newLeader->IsType( idActor::GetClassType() ) ) {
gameLocal.Error( "'%s' is not an idActor (player or ai controlled character)", newLeader->name.c_str() );
} else {
leader = static_cast<idActor *>( newLeader );
}
if ( oldLeader != leader ) {
OnLeaderChange ( oldLeader );
}
}
/***********************************************************************
Head & torso aiming
***********************************************************************/
/*
================
idAI::UpdateAnimationControllers
================
*/
bool idAI::UpdateAnimationControllers( void ) {
idVec3 left;
idVec3 dir;
idVec3 orientationJointPos;
idVec3 localDir;
idAngles newLookAng;
idMat3 mat;
idMat3 axis;
idMat3 orientationJointAxis;
idVec3 pos;
int i;
idAngles jointAng;
float orientationJointYaw;
float currentHeadFocusRate = ( combat.fl.aware ? headFocusRate : headFocusRate * 0.5f );
MEM_SCOPED_TAG(tag,MA_ANIM);
if ( aifl.dead ) {
return idActor::UpdateAnimationControllers();
}
if ( orientationJoint == INVALID_JOINT ) {
orientationJointAxis = viewAxis;
orientationJointPos = physicsObj.GetOrigin();
orientationJointYaw = move.current_yaw;
} else {
GetJointWorldTransform( orientationJoint, gameLocal.time, orientationJointPos, orientationJointAxis );
orientationJointYaw = orientationJointAxis[ 2 ].ToYaw();
orientationJointAxis = idAngles( 0.0f, orientationJointYaw, 0.0f ).ToMat3();
}
// Update the IK after we've gotten all the joint positions we need, but before we set any joint positions.
// Getting the joint positions causes the joints to be updated. The IK gets joint positions itself (which
// are already up to date because of getting the joints in this function) and then sets their positions, which
// forces the heirarchy to be updated again next time we get a joint or present the model. If IK is enabled,
// or if we have a seperate head, we end up transforming the joints twice per frame. Characters with no
// head entity and no ik will only transform their joints once. Set g_debuganim to the current entity number
// in order to see how many times an entity transforms the joints per frame.
idActor::UpdateAnimationControllers();
// Update the focus position
UpdateFocus( orientationJointAxis );
//MCG NOTE: don't know why Dube added this extra check for the torsoAnim (5/09/05), but it was causing popping, so I took it out... :/
//bool canLook = ( !torsoAnim.AnimDone(0) || !torsoAnim.GetAnimator()->CurrentAnim(ANIMCHANNEL_TORSO)->IsDone(gameLocal.GetTime()) || torsoAnim.Disabled() ) && !torsoAnim.GetAnimFlags().ai_no_look && !aifl.disableLook;
//bool canLook = (!torsoAnim.GetAnimFlags().ai_no_look && !aifl.disableLook);
bool canLook = (!animator.GetAnimFlags(animator.CurrentAnim(ANIMCHANNEL_TORSO)->AnimNum()).ai_no_look && !aifl.disableLook);
if ( !canLook ) {
//actually, do the looking, but bring it back forward...
currentFocusPos = GetEyePosition() + orientationJointAxis[ 0 ] * 64.0f;
}
// Determine the new look yaw
dir = currentFocusPos - orientationJointPos;
newLookAng.yaw = dir.ToYaw( );
// Determine the new look pitch
dir = currentFocusPos - GetEyePosition();
dir.NormalizeFast();
orientationJointAxis.ProjectVector( dir, localDir );
newLookAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() );
newLookAng.roll = 0.0f;
if ( !canLook ) {
//actually, do the looking, but bring it back forward...
newLookAng.yaw = orientationJointAxis[0].ToYaw();
newLookAng.pitch = 0;
}
// Add the angle change using the head focus rate and convert the look angles to
// local angles so they can be properly clamped.
float f = lookAng.yaw;
if ( lookMin.yaw <= -360.0f && lookMax.yaw >= 360.0f )
{
lookAng.yaw = f - orientationJointYaw;
float diff;
diff = ( idMath::AngleNormalize360(newLookAng.yaw) - idMath::AngleNormalize360(f) );
diff = idMath::AngleNormalize180( diff );
lookAng.yaw += diff * currentHeadFocusRate;
//lookAng.yaw += ( newLookAng.yaw - f ) * currentHeadFocusRate;
}
else
{
lookAng.yaw = idMath::AngleNormalize180 ( f - orientationJointYaw );
lookAng.yaw += (idMath::AngleNormalize180 ( newLookAng.yaw - f ) * currentHeadFocusRate);
}
lookAng.pitch += ( idMath::AngleNormalize180( newLookAng.pitch - lookAng.pitch ) * currentHeadFocusRate );
// Clamp the look angles
lookAng.Clamp( lookMin, lookMax );
// Calcuate the eye angles
f = eyeAng.yaw;
eyeAng.yaw = idMath::AngleNormalize180( f - orientationJointYaw );
eyeAng.yaw += (idMath::AngleNormalize180( newLookAng.yaw - f ) * eyeFocusRate);
eyeAng.pitch += (idMath::AngleNormalize180( newLookAng.pitch - eyeAng.pitch ) * eyeFocusRate);
// Clamp eye angles relative to the look angles
jointAng = eyeAng - lookAng;
jointAng.Normalize180( );
jointAng.Clamp( eyeMin, eyeMax );
eyeAng = lookAng + jointAng;
eyeAng.Normalize180( );
if ( canLook ) {
// Apply the look angles to the look joints
if ( animator.GetAnimFlags( animator.CurrentAnim( ANIMCHANNEL_TORSO )->AnimNum()).ai_look_head_only ) {
if ( neckJoint != INVALID_JOINT || headJoint != INVALID_JOINT ) {
float jScale = 1.0f;
if ( neckJoint != INVALID_JOINT && headJoint != INVALID_JOINT ) {
jScale = 0.5f;
}
jointAng.pitch = lookAng.pitch * jScale;
jointAng.yaw = lookAng.yaw * jScale;
if ( neckJoint != INVALID_JOINT ) {
animator.SetJointAxis( neckJoint, JOINTMOD_WORLD, jointAng.ToMat3() );
}
if ( headJoint != INVALID_JOINT ) {
animator.SetJointAxis( headJoint, JOINTMOD_WORLD, jointAng.ToMat3() );
}
}
//what if we have a previous joint mod on the rest of the lookJoints
//from an anim that *wasn't* ai_look_head_only...?
//just clear them? Or move them towards 0?
//FIXME: move them towards zero...
if ( newLookAng.Compare( lookAng ) ) {
//snap back now!
for( i = 0; i < lookJoints.Num(); i++ ) {
if ( lookJoints[i] != neckJoint
&& lookJoints[i] != headJoint ) {
//snap back now!
animator.ClearJoint ( lookJoints[i] );
}
}
} else {
//blend back
//yes, this is framerate dependant and inefficient and wrong, but... eliminates pops
jointAng.roll = 0.0f;
jointMod_t *curJointMod;
idAngles curJointAngleMod;
for( i = 0; i < lookJoints.Num(); i++ ) {
if ( lookJoints[i] != neckJoint
&& lookJoints[i] != headJoint ) {
//blend back
curJointMod = animator.FindExistingJointMod( lookJoints[ i ], NULL );
if ( curJointMod )
{
curJointAngleMod = curJointMod->mat.ToAngles();
curJointAngleMod *= 0.75f;
if ( fabs(curJointAngleMod.pitch) < 1.0f
&& fabs(curJointAngleMod.yaw) < 1.0f
&& fabs(curJointAngleMod.roll) < 1.0f )
{
//snap back now!
animator.ClearJoint ( lookJoints[i] );
}
else
{
animator.SetJointAxis( lookJoints[ i ], JOINTMOD_WORLD, curJointAngleMod.ToMat3() );
}
}
}
}
}
} else {
jointAng.roll = 0.0f;
for( i = 0; i < lookJoints.Num(); i++ ) {
jointAng.pitch = lookAng.pitch * lookJointAngles[ i ].pitch;
jointAng.yaw = lookAng.yaw * lookJointAngles[ i ].yaw;
animator.SetJointAxis( lookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() );
}
}
} else {
//if ( animator.GetAnimFlags(animator.CurrentAnim(ANIMCHANNEL_TORSO)->AnimNum()).ai_no_look || aifl.disableLook ) {
if ( newLookAng.Compare( lookAng ) ) {
//snap back now!
for( i = 0; i < lookJoints.Num(); i++ ) {
animator.ClearJoint ( lookJoints[i] );
}
} else {
//PCJ back to neutral
jointAng.roll = 0.0f;
for( i = 0; i < lookJoints.Num(); i++ ) {
jointAng.pitch = lookAng.pitch * lookJointAngles[ i ].pitch;
jointAng.yaw = lookAng.yaw * lookJointAngles[ i ].yaw;
animator.SetJointAxis( lookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() );
}
}
}
// Convert the look angles back to world angles. This is done to prevent dramatic orietation changes
// from changing the look direction abruptly (unless of course the minimums are not met)
lookAng.yaw = idMath::AngleNormalize180( lookAng.yaw + orientationJointYaw );
eyeAng.yaw = idMath::AngleNormalize180( eyeAng.yaw + orientationJointYaw );
if ( move.moveType == MOVETYPE_FLY || move.fl.flyTurning ) {
// lean into turns
AdjustFlyingAngles();
}
// Orient the eyes towards their target
if ( leftEyeJoint != INVALID_JOINT && rightEyeJoint != INVALID_JOINT ) {
if ( head ) {
idAnimator *headAnimator = head->GetAnimator();
if ( focusType != AIFOCUS_NONE && allowEyeFocus && canLook ) {
//tweak these since it's looking at the wrong spot and not calculating from each eye and I don't have time to bother rewriting all of this properly
eyeAng.yaw -= 0.5f;
eyeAng.pitch += 3.25f;
idMat3 eyeAxis = ( eyeAng ).ToMat3() * head->GetPhysics()->GetAxis().Transpose ( );
headAnimator->SetJointAxis ( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyeAxis );
eyeAng.yaw += 3.0f;
eyeAxis = ( eyeAng ).ToMat3() * head->GetPhysics()->GetAxis().Transpose ( );
headAnimator->SetJointAxis ( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyeAxis );
if ( ai_debugEyeFocus.GetBool() ) {
idVec3 eyeOrigin;
idMat3 axis;
head->GetJointWorldTransform ( rightEyeJoint, gameLocal.time, eyeOrigin, axis );
gameRenderWorld->DebugArrow ( colorGreen, eyeOrigin, ( eyeOrigin + (32.0f*axis[0])), 1, 0 );
head->GetJointWorldTransform ( leftEyeJoint, gameLocal.time, eyeOrigin, axis );
gameRenderWorld->DebugArrow ( colorGreen, eyeOrigin, ( eyeOrigin + (32.0f*axis[0])), 1, 0 );
}
} else {
headAnimator->ClearJoint( leftEyeJoint );
headAnimator->ClearJoint( rightEyeJoint );
}
} else {
if ( allowEyeFocus && focusType != AIFOCUS_NONE && !torsoAnim.GetAnimFlags().ai_no_look && !aifl.disableLook ) {
idMat3 eyeAxis = ( eyeAng ).ToMat3() * GetPhysics()->GetAxis().Transpose ( );
animator.SetJointAxis ( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyeAxis );
animator.SetJointAxis ( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyeAxis );
} else {
animator.ClearJoint( leftEyeJoint );
animator.ClearJoint( rightEyeJoint );
}
}
}
return true;
}
void idAI::OnTouch( idEntity *other, trace_t *trace ) {
// if we dont have an enemy or had one for at least a second that is a potential enemy the set the enemy
if ( other->IsType( idActor::GetClassType() )
&& !other->fl.notarget
&& ( ReactionTo( other )&ATTACK_ON_SIGHT)
&& (!enemy.ent || gameLocal.time - enemy.changeTime > 1000 ) ) {
SetEnemy( other );
}
if ( !enemy.ent && !other->fl.notarget && ( ReactionTo( other ) & ATTACK_ON_ACTIVATE ) ) {
Activate( other );
}
pusher = other;
aifl.pushed = true;
// If pushed by the player update tactical
if ( pusher && pusher->IsType ( idPlayer::GetClassType() ) && (combat.tacticalMaskAvailable & AITACTICAL_MOVE_PLAYERPUSH_BIT) ) {
ForceTacticalUpdate ( );
}
}
idProjectile* idAI::AttackProjectile ( const idDict* projectileDict, const idVec3 &org, const idAngles &ang ) {
idVec3 start;
trace_t tr;
idBounds projBounds;
const idClipModel* projClip;
idMat3 axis;
float distance;
idProjectile* result;
if ( !projectileDict ) {
gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() );
return NULL;
}
axis = ang.ToMat3();
if ( !projectile.GetEntity() ) {
CreateProjectile( projectileDict, org, axis[ 0 ] );
}
// make sure the projectile starts inside the monster bounding box
const idBounds &ownerBounds = physicsObj.GetAbsBounds();
projClip = projectile.GetEntity()->GetPhysics()->GetClipModel();
projBounds = projClip->GetBounds().Rotate( projClip->GetAxis() );
// check if the owner bounds is bigger than the projectile bounds
if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) &&
( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) &&
( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) {
if ( (ownerBounds - projBounds).RayIntersection( org, viewAxis[ 0 ], distance ) ) {
start = org + distance * viewAxis[ 0 ];
} else {
start = ownerBounds.GetCenter();
}
} else {
// projectile bounds bigger than the owner bounds, so just start it from the center
start = ownerBounds.GetCenter();
}
// RAVEN BEGIN
// ddynerman: multiple clip worlds
gameLocal.Translation( this, tr, start, org, projClip, projClip->GetAxis(), MASK_SHOT_RENDERMODEL, this );
// RAVEN END
// launch the projectile
projectile.GetEntity()->Launch( tr.endpos, axis[ 0 ], vec3_origin );
result = projectile;
projectile = NULL;
lastAttackTime = gameLocal.time;
return result;
}
void idAI::RadiusDamageFromJoint( const char *jointname, const char *damageDefName ) {
jointHandle_t joint;
idVec3 org;
idMat3 axis;
if ( !jointname || !jointname[ 0 ] ) {
org = physicsObj.GetOrigin();
} else {
joint = animator.GetJointHandle( jointname );
if ( joint == INVALID_JOINT ) {
gameLocal.Error( "Unknown joint '%s' on %s", jointname, GetEntityDefName() );
}
GetJointWorldTransform( joint, gameLocal.time, org, axis );
}
gameLocal.RadiusDamage( org, this, this, this, this, damageDefName );
}
/*
=====================
idAI::CanBecomeSolid
returns true if the AI entity could become solid at its current position
=====================
*/
bool idAI::CanBecomeSolid ( void ) {
int i;
int num;
idEntity * hit;
idClipModel* cm;
idClipModel* clipModels[ MAX_GENTITIES ];
// Determine what we are currently touching
// RAVEN BEGIN
// ddynerman: multiple clip worlds
num = gameLocal.ClipModelsTouchingBounds( this, physicsObj.GetAbsBounds(), MASK_MONSTERSOLID, clipModels, MAX_GENTITIES );
// RAVEN END
for ( i = 0; i < num; i++ ) {
cm = clipModels[ i ];
// don't check render entities
if ( cm->IsRenderModel() ) {
continue;
}
hit = cm->GetEntity();
if ( ( hit == this ) || !hit->fl.takedamage ) {
continue;
}
if ( physicsObj.ClipContents( cm ) ) {
return false;
}
}
return true;
}
/*
=====================
idAI::BecomeSolid
=====================
*/
void idAI::BecomeSolid( void ) {
physicsObj.EnableClip();
if ( spawnArgs.GetBool( "big_monster" ) ) {
physicsObj.SetContents( 0 );
} else if ( use_combat_bbox ) {
physicsObj.SetContents( CONTENTS_BODY|CONTENTS_SOLID );
} else {
physicsObj.SetContents( CONTENTS_BODY );
}
// RAVEN BEGIN
// ddynerman: multiple clip worlds
physicsObj.GetClipModel()->Link();
// RAVEN END
fl.takedamage = !spawnArgs.GetBool( "noDamage" );
}
/*
=====================
idAI::BecomeNonSolid
=====================
*/
void idAI::BecomeNonSolid( void ) {
fl.takedamage = false;
physicsObj.SetContents( 0 );
physicsObj.GetClipModel()->Unlink();
}
const char *idAI::ChooseAnim( int channel, const char *animname ) {
int anim;
anim = GetAnim( channel, animname );
if ( anim ) {
if ( channel == ANIMCHANNEL_HEAD ) {
if ( head.GetEntity() ) {
return head.GetEntity()->GetAnimator()->AnimFullName( anim );
}
} else {
return animator.AnimFullName( anim );
}
}
return "";
}
/*
============
idAI::ExecScriptFunction
============
*/
void idAI::ExecScriptFunction ( rvScriptFuncUtility& func, idEntity* parm ) {
if( parm ) {
func.InsertEntity( parm, 0 );
} else {
func.InsertEntity( this, 0 );
}
func.CallFunc( &spawnArgs );
if( parm ) {
func.RemoveIndex( 0 );
}
}
/*
============
idAI::Prethink
============
*/
void idAI::Prethink ( void ) {
// Update our helper if we are moving
if ( move.fl.moving ) {
UpdateHelper ( );
}
if ( leader ) {
idEntity* groundEnt = leader->GetGroundEntity ( );
if ( !(tether && !aifl.tetherMover) && groundEnt ) {
if ( groundEnt->IsType ( idMover::GetClassType ( ) ) ) {
idEntity* ent;
idEntity* next;
for( ent = groundEnt->GetNextTeamEntity(); ent != NULL; ent = next ) {
next = ent->GetNextTeamEntity();
if ( ent->GetBindMaster() == groundEnt && ent->IsType ( rvAITether::GetClassType ( ) ) ) {
SetTether ( static_cast<rvAITether*>(ent) );
aifl.tetherMover = true;
break;
}
}
} else {
SetTether ( NULL );
}
}
} else if ( tether && aifl.tetherMover ) {
SetTether ( NULL );
}
}
/*
============
idAI::Postthink
============
*/
void idAI::Postthink( void ){
if ( !aifl.simpleThink ) {
pain.takenThisFrame = 0;
}
// Draw debug tactical information
DrawTactical ( );
if( vehicleController.IsDriving() ){ // Generate some sort of command?
usercmd_t usercmd;
// Note! usercmd angles stuff is in deltas, not in absolute values.
memset( &usercmd, 0, sizeof( usercmd ) );
idVec3 toEnemy;
if( enemy.ent ){
toEnemy = enemy.ent->GetPhysics()->GetOrigin();
toEnemy -= GetPhysics()->GetOrigin();
toEnemy.Normalize();
idAngles enemyAng;
enemyAng = toEnemy.ToAngles();
usercmd.angles[PITCH] = ANGLE2SHORT( enemyAng.pitch );
usercmd.angles[YAW] = ANGLE2SHORT( enemyAng.yaw );
usercmd.buttons = BUTTON_ATTACK;
vehicleController.SetInput ( usercmd, enemyAng );
}
}
// Keep our threat value up to date
UpdateThreat ( );
}
/*
============
idAI::
============
*/
void idAI::OnDeath( void ){
if( vehicleController.IsDriving() ){
usercmd_t usercmd;
memset( &usercmd, 0, sizeof( usercmd ) );
usercmd.buttons = BUTTON_ATTACK;
usercmd.upmove = 300.0f; // This will cause the character to eject.
vehicleController.SetInput( usercmd, idAngles( 0, 0, 0 ) );
// Fixme! Is this safe to do immediately?
vehicleController.Eject();
}
aiManager.RemoveTeammate ( this );
ExecScriptFunction( funcs.death );
/* DONT DROP ANYTHING FOR NOW
float rVal = gameLocal.random.RandomInt( 100 );
if( spawnArgs.GetFloat( "no_drops" ) >= 1.0 ){
spawnArgs.Set( "def_dropsItem1", "" );
}else{
// Fixme! Better guys should drop better stuffs! Make drops related to guy type? Do something cooler here?
if( rVal < 25 ){ // Half of guys drop nothing?
spawnArgs.Set( "def_dropsItem1", "" );
}else if( rVal < 50 ){
spawnArgs.Set( "def_dropsItem1", "item_health_small" );
}
}
*/
}
/*
============
idAI::OnWakeUp
============
*/
void idAI::OnWakeUp ( void ) {
}
/*
============
idAI::OnUpdatePlayback
============
*/
void idAI::OnUpdatePlayback ( const rvDeclPlaybackData& pbd ) {
return;
}
/*
============
idAI::OnLeaderChange
============
*/
void idAI::OnLeaderChange ( idEntity* oldLeader ) {
ForceTacticalUpdate ( );
}
/*
============
idAI::OnEnemyChange
============
*/
void idAI::OnEnemyChange ( idEntity* oldEnemy ) {
// Make sure we update our tactical state immediately
ForceTacticalUpdate ( );
// see if we should announce the enemy
if ( enemy.ent ) {
combat.fl.aware = true;
combat.investigateTime = 0;
enemy.changeTime = gameLocal.time;
enemy.lastKnownPosition = enemy.ent->GetPhysics()->GetOrigin ( );
UpdateEnemyVisibility ( );
UpdateEnemyPosition ( true );
UpdateEnemy ( );
} else {
enemy.range = 0;
enemy.range2d = 0;
enemy.changeTime = 0;
enemy.smoothedLinearVelocity.Zero ( );
enemy.smoothedPushedVelocity.Zero ( );
}
enemy.fl.visible = false;
}
/*
============
idAI::OnTacticalChange
============
*/
void idAI::OnTacticalChange ( aiTactical_t oldTactical ) {
// if acutally moving to a new tactial location announce it
if ( move.fl.moving ) {
AnnounceTactical( combat.tacticalCurrent );
}
}
/*
============
idAI::OnFriendlyFire
============
*/
void idAI::OnFriendlyFire ( idActor* attacker ) {
AnnounceFriendlyFire( static_cast<idActor*>(attacker) );
}
/*
============
idAI::OnStartMoving
============
*/
void idAI::OnStartMoving ( void ) {
aifl.simpleThink = false;
combat.fl.crouchViewClear = false;
}
/*
============
idAI::OnStopMoving
============
*/
void idAI::OnStopMoving ( aiMoveCommand_t oldMoveCommand ) {
}
/*
============
idAI::OnStartAction
============
*/
void idAI::OnStartAction ( void ) {
}
/*
============
idAI::OnStopAction
============
*/
void idAI::OnStopAction ( void ) {
}
/*
============
idAI::OnEnemyVisiblityChange
============
*/
void idAI::OnEnemyVisiblityChange ( bool oldVisible ) {
}
/*
============
idAI::OnSetKey
============
*/
void idAI::OnSetKey ( const char* key, const char* value ) {
if ( !idStr::Icmp ( key, "noCombatChatter" ) ) {
combat.fl.noChatter = spawnArgs.GetBool ( key );
} else if ( !idStr::Icmp ( key, "allowPlayerPush" ) ) {
combat.tacticalMaskAvailable &= ~(AITACTICAL_MOVE_PLAYERPUSH_BIT);
if ( spawnArgs.GetBool ( key ) ) {
combat.tacticalMaskAvailable |= AITACTICAL_MOVE_PLAYERPUSH_BIT;
}
} else if ( !idStr::Icmp ( key, "noLook" ) ) {
aifl.disableLook = spawnArgs.GetBool ( key );
} else if ( !idStr::Icmp ( key, "killer_guard" ) ) {
aifl.killerGuard = spawnArgs.GetBool ( key );
}
}
/*
============
idAI::OnCoverInvalidated
============
*/
void idAI::OnCoverInvalidated ( void ) {
// Force a tactical update now
ForceTacticalUpdate ( );
}
/*
============
idAI::OnCoverNotFacingEnemy
============
*/
void idAI::OnCoverNotFacingEnemy ( void ) {
// Clear attack timers so we can shoot right now
actionTimerRangedAttack.Clear ( actionTime );
actionRangedAttack.timer.Clear( actionTime );
}
/*
============
idAI::SkipCurrentDestination
Is the AI's current destination ok enough to stay at?
============
*/
bool idAI::SkipCurrentDestination ( void ) const {
// can only skip current destination when we are stopped
if ( move.fl.moving ) {
return false;
}
/*
// If we are currently behind cover and that cover is no longer valid we should skip it
if ( IsBehindCover ( ) && !IsCoverValid ( ) ) {
return true;
}
*/
return false;
}
/*
============
idAI::SkipImpulse
============
*/
bool idAI::SkipImpulse( idEntity *ent, int id ){
bool skip = idActor::SkipImpulse( ent, id );
if( af.IsActive ( ) ) {
return false;
}
if( !fl.takedamage ){
return true;
}
if( move.moveCommand == MOVE_RV_PLAYBACK ){
return true;
}
return skip;
}
/*
============
idAI::CanHitEnemy
============
*/
bool idAI::CanHitEnemy ( void ) {
trace_t tr;
idEntity *hit;
idEntity *enemyEnt = enemy.ent;
if ( !IsEnemyVisible ( ) || !enemyEnt ) {
return false;
}
// don't check twice per frame
if ( gameLocal.time == lastHitCheckTime ) {
return lastHitCheckResult;
}
lastHitCheckTime = gameLocal.time;
idVec3 toPos = enemyEnt->GetEyePosition();
idVec3 eye = GetEyePosition();
idVec3 dir;
// expand the ray out as far as possible so we can detect anything behind the enemy
dir = toPos - eye;
dir.Normalize();
toPos = eye + dir * MAX_WORLD_SIZE;
// RAVEN BEGIN
// ddynerman: multiple clip worlds
if ( g_perfTest_aiNoVisTrace.GetBool() ) {
lastHitCheckResult = true;
return lastHitCheckResult;
}
gameLocal.TracePoint( this, tr, eye, toPos, MASK_SHOT_BOUNDINGBOX, this );
hit = gameLocal.GetTraceEntity( tr );
// RAVEN END
if ( tr.fraction >= 1.0f || ( hit == enemyEnt ) ) {
lastHitCheckResult = true;
} else if ( ( tr.fraction < 1.0f ) && ( hit->IsType( idAI::Type ) ) &&
( static_cast<idAI *>( hit )->team != team ) ) {
lastHitCheckResult = true;
} else {
lastHitCheckResult = false;
}
return lastHitCheckResult;
}
/*
============
idAI::CanHitEnemyFromJoint
============
*/
bool idAI::CanHitEnemyFromJoint( const char *jointname ){
trace_t tr;
idVec3 muzzle;
idMat3 axis;
idEntity *enemyEnt = enemy.ent;
if ( !IsEnemyVisible ( ) || !enemyEnt ) {
return false;
}
// don't check twice per frame
if ( gameLocal.time == lastHitCheckTime ) {
return lastHitCheckResult;
}
if ( g_perfTest_aiNoVisTrace.GetBool() ) {
lastHitCheckResult = true;
return true;
}
lastHitCheckTime = gameLocal.time;
idVec3 toPos = enemyEnt->GetEyePosition();
jointHandle_t joint = animator.GetJointHandle( jointname );
if ( joint == INVALID_JOINT ) {
gameLocal.Error( "Unknown joint '%s' on %s", jointname, GetEntityDefName() );
}
animator.GetJointTransform( joint, gameLocal.time, muzzle, axis );
muzzle = physicsObj.GetOrigin() + ( muzzle + modelOffset ) * viewAxis * physicsObj.GetGravityAxis();
// RAVEN BEGIN
// ddynerman: multiple clip worlds
gameLocal.TracePoint( this, tr, muzzle, toPos, MASK_SHOT_BOUNDINGBOX, this );
// RAVEN END
if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == enemyEnt ) ) {
lastHitCheckResult = true;
} else {
lastHitCheckResult = false;
}
return lastHitCheckResult;
}
/*
============
idAI::
============
float HeightForTrajectory( const idVec3 &start, float zVel, float gravity );
int idAI::TestTrajectory( const idVec3 &firePos, const idVec3 &target, const char *projectileName ){
idVec3 projVelocity;
idVec3 projGravity;
float testTime;
float zVel, height, pitch, s, c;
idVec3 dir;
float newVel;
float delta_x;
float delta_z;
projectileDef = gameLocal.FindEntityDefDict ( projectileName );
projVelocity = idProjectile::GetVelocity( projectileDef );
projGravity = idProjectile::GetGravity( projectileDef ).z * GetPhysics()->GetGravity();
pitch = DEG2RAD( gameLocal.random.RandomFloat() * 50 + 20 ); // Random pitch range between 20 and 70. Should this be customizeable?
idMath::SinCos( pitch, s, c );
delta_x = idMath::Sqrt( ( target.x - firePos.x ) * ( target.x - firePos.x ) + ( target.y - firePos.y ) * ( target.y - firePos.y ) );
delta_z = target.z - firePos.z;
newVel = ( delta_x / idMath::Cos( pitch ) ) * idMath::Sqrt( projGravity.z / ( 2.0f * ( delta_x * idMath::Tan( pitch ) - delta_z ) ) );
testTime = delta_x / ( newVel * c );
zVel = newVel * s;
float a = idMath::ASin ( delta_x * GetPhysics()->GetGravity().Length() / (projVelocity.x * projVelocity.x) );
a = a / 2;
float r = (projVelocity.x * projVelocity.x) * idMath::Sin ( 2 * a ) / GetPhysics()->GetGravity().Length();
if ( r < delta_x - (delta_x * 0.1) ) {
mVar.valid_lobbed_shot = 0;
return 0;
} else {
mVar.lobDir = target - firePos;
mVar.lobDir.z = 0;
mVar.lobDir.z = idMath::Tan ( a ) * mVar.lobDir.LengthFast();
mVar.lobDir.Normalize ( );
mVar.valid_lobbed_shot = gameLocal.time;
mVar.lob_vel_scale = 1.0f; // newVel / projVelocity.Length();
return 1;
}
projGravity[2] *= -1.0f;
dir = target - firePos;
dir.z = 0;
dir.Normalize();
delta_x = idMath::Sqrt( 1 - ( c * c ) );
dir.x *= delta_x;
dir.y *= delta_x;
dir.z = c;
height = HeightForTrajectory( firePos, zVel, projGravity[2] ) - firePos.z;
if ( height > MAX_WORLD_SIZE ) {
// goes higher than we want to allow
mVar.valid_lobbed_shot = 0;
return 0;
}
if ( idAI::TestTrajectory ( firePos, target, zVel, projGravity[2], testTime, height * 2, NULL,
MASK_SHOT_RENDERMODEL, this, enemy.GetEntity(), ai_debugTrajectory.GetBool() ? 4000 : 0 ) ) {
if ( ai_debugTrajectory.GetBool() ) {
float t = testTime / 100.0f;
idVec3 velocity = dir * newVel;
idVec3 lastPos, pos;
lastPos = firePos;
pos = firePos;
for ( int j = 1; j < 100; j++ ) {
pos += velocity * t;
velocity += projGravity * t;
gameRenderWorld->DebugLine( colorCyan, lastPos, pos, ai_debugTrajectory.GetBool() ? 4000 : 0 );
lastPos = pos;
}
}
mVar.lobDir = dir;
mVar.valid_lobbed_shot = gameLocal.time;
mVar.lob_vel_scale = newVel / projVelocity.Length();
}else{
mVar.valid_lobbed_shot = 0;
}
return ( (int)mVar.valid_lobbed_shot != 0 );
}
*/
/*
============
idAI::
============
*/
float idAI::GetTurnDelta( void ){
float amount;
if ( move.turnRate ) {
amount = idMath::AngleNormalize180( move.ideal_yaw - move.current_yaw );
return amount;
} else {
return 0.0f;
}
}
/*
============
idAI::GetIdleAnimName
============
*/
const char* idAI::GetIdleAnimName ( void ) {
const char* animName = NULL;
// Start idle animation
if ( enemy.ent ) {
animName = "idle_alert";
}
if ( animName && HasAnim ( ANIMCHANNEL_ALL, animName ) ) {
return animName;
}
return "idle";
}
/*
===============================================================================
idAI - Enemy Finding
===============================================================================
*/
/*
============
idAI::FindEnemy
============
*/
idEntity *idAI::FindEnemy ( bool inFov, bool forceNearest, float maxDistSqr ){
idActor* actor;
idActor* bestEnemy;
idActor* bestEnemyBackup;
float bestThreat;
float bestThreatBackup;
float distSqr;
float enemyRangeSqr;
float awareRangeSqr;
idVec3 origin;
idVec3 delta;
pvsHandle_t pvs;
// Setup our local variables used in the search
pvs = gameLocal.pvs.SetupCurrentPVS( GetPVSAreas(), GetNumPVSAreas() );
bestThreat = 0.0f;
bestThreatBackup = 0.0f;
bestEnemy = NULL;
bestEnemyBackup = NULL;
awareRangeSqr = Square ( combat.awareRange );
enemyRangeSqr = enemy.ent ? Square ( enemy.range ) : Square ( combat.attackRange[1] );
origin = GetEyePosition ( );
// Iterate through the enemy team
for( actor = aiManager.GetEnemyTeam ( (aiTeam_t)team ); actor; actor = actor->teamNode.Next() ) {
// Skip hidden enemies and enemies that cant be targeted
if( actor->fl.notarget || actor->fl.isDormant || ( actor->IsHidden ( ) && !actor->IsInVehicle() ) ) {
continue;
}
// Calculate the distance between ourselves and our potential enemy
delta = physicsObj.GetOrigin() - actor->GetPhysics()->GetOrigin();
distSqr = delta.LengthSqr();
// Calculate the adjusted threat for this actor
float threat = CalculateEnemyThreat ( actor );
// Save the highest threat enemy as a backup in case we cannot find one we can see
if ( threat > bestThreatBackup ) {
bestThreatBackup = threat;
bestEnemyBackup = actor;
}
// If we have already found a more threatening enemy then attack that
if ( threat < bestThreat ) {
continue;
}
// If this enemy isnt in the same pvps then use them as a backup
if ( !gameLocal.pvs.InCurrentPVS( pvs, actor->GetPVSAreas(), actor->GetNumPVSAreas() ) ) {
continue;
}
// this is pretty specific to Quake 4, but we don't want the player to be around an enemy too long who can't see him. We need to randomly spike the
// awareRange on creatures to simulate them looking behind them, or noticing someone standing around for too long.
// Modders take note, this will prevent most "sneaking up on bad guys" action because they will likely spike their aware ranges out
// during the sneaking.
if( gameLocal.random.RandomFloat() < 0.005f ) {
awareRangeSqr *= 15;
}
// fov doesn't matter if they're within awareRange, we "sense" them if we're alert... (or should LOS not even matter at this point?)
if ( distSqr < awareRangeSqr || CanSeeFrom ( origin, actor, (inFov && !(combat.fl.aware&&distSqr<awareRangeSqr)) ) ) {
bestThreat = threat;
bestEnemy = actor;
}
}
// If force nearest is set we will give them an enemy reguardless of distance or sight
if( forceNearest ){
if( bestEnemy == NULL ){
bestEnemy = bestEnemyBackup;
}
}
gameLocal.pvs.FreeCurrentPVS( pvs );
return bestEnemy;
}
/*
============
idAI::CheckForEnemy
Look for a suitable enemy
============
*/
bool idAI::CheckForEnemy ( bool useFov, bool force ) {
idEntity *newEnemy;
if ( combat.fl.ignoreEnemies ) {
return false;
}
// If we already have an enemy and arent being forced to find a new on then just return now
if ( enemy.ent && !force ) {
return true;
}
// Save last time we checked for a new enemy
enemy.checkTime = gameLocal.time;
// Dont use fov check when behind cover because you are up against a wall
newEnemy = FindEnemy ( !IsBehindCover ( ), 0, 0.0f );
// Havent found an enemy yet, see if we heard something that can be our enemy
if ( !newEnemy ) {
newEnemy = HeardSound( true );
}
// If we still havent found an enemy, see if a teammate can give us one
if ( !newEnemy ) {
return CheckForTeammateEnemy ( );
}
SetEnemy( newEnemy );
return true;
}
/*
============
idAI::CheckForTeammateEnemy
============
*/
bool idAI::CheckForTeammateEnemy( void ) {
idActor* teammate;
idEntity* teammateEnemy;
// Not looking for a new enemy.
if ( combat.fl.ignoreEnemies ) {
return false;
}
// Find an enemy from a nearby ally
teammateEnemy = aiManager.NearestTeammateEnemy ( this, 1000.0f, false, false, &teammate );
if ( !teammateEnemy || teammateEnemy == enemy.ent ) {
return false;
}
assert ( teammate );
// Attempt to set the new enemy
if ( !SetEnemy ( teammateEnemy ) ) {
return false;
}
// If the ally is another AI entity we can use their enemy visibility information
if ( teammate->IsType ( idAI::Type ) ) {
idAI* teammateAI = static_cast<idAI*>(teammate);
enemy.smoothedLinearVelocity = teammateAI->enemy.smoothedLinearVelocity;
enemy.smoothedPushedVelocity = teammateAI->enemy.smoothedPushedVelocity;
enemy.lastKnownPosition = teammateAI->enemy.lastKnownPosition;
enemy.lastVisibleEyePosition = teammateAI->enemy.lastVisibleEyePosition;
enemy.lastVisibleFromEyePosition = teammateAI->enemy.lastVisibleEyePosition;
enemy.lastVisibleChestPosition = teammateAI->enemy.lastVisibleChestPosition;
enemy.lastVisibleTime = 0;
}
return true;
}
/*
============
idAI::CheckForCloserEnemy
============
*/
bool idAI::CheckForCloserEnemy ( void ) {
idEntity* newEnemy = NULL;
float maxDistSqr;
// Not looking for a new enemy.
if ( combat.fl.ignoreEnemies ) {
return false;
}
// See if we happen to have heard someone this frame that we can use
newEnemy = HeardSound( true );
if ( newEnemy && newEnemy != enemy.ent && newEnemy->IsType( idActor::GetClassType() ) ) {
//heard someone else!
float newDist = DistanceTo ( newEnemy->GetPhysics()->GetOrigin() );
// Are they closer than the enemy we are fighting?
if ( newDist < enemy.range ) {
//new enemy is closer than current one, take them!
SetEnemy( newEnemy );
return true;
}
}
if ( GetEnemy() && enemy.range ) {
maxDistSqr = Min( Square ( enemy.range ), Square ( combat.awareRange ) );
} else {
maxDistSqr = Square ( combat.awareRange );
}
newEnemy = FindEnemy( false, 0, maxDistSqr );
if ( !newEnemy ) {
return false;
}
SetEnemy( newEnemy );
return true;
}
/*
============
idAI::CheckForReplaceEnemy
TODO: Call CalculateThreat ( ent ) and compare to current entity
============
*/
bool idAI::CheckForReplaceEnemy ( idEntity* replacement ) {
bool replace;
// If our replacement is a driver a vehicle and they are hidden we will
// want to shoot back at their vehicle not them.
idActor* actor;
actor = dynamic_cast<idActor*>(replacement);
if ( actor && actor->IsInVehicle ( ) && actor->IsHidden ( ) ) {
replacement = actor->GetVehicleController ( ).GetVehicle ( );
}
// Invalid replacement?
if ( !replacement) {
return false;
}
// Not looking for a new enemy.
if ( combat.fl.ignoreEnemies ) {
return false;
}
if ( replacement == enemy.ent ) {
return false;
}
// Dont want to set our enemy to a friendly target
if ( replacement->IsType( idActor::GetClassType() ) && (static_cast<idActor*>(replacement))->team == team ) {
return false;
}
// Not having an enemy will set it immediately
if ( !enemy.ent ) {
SetEnemy ( replacement );
return true;
}
// Dont change enemies too often when being hit
if ( gameLocal.time - enemy.changeTime < 1000 ) {
return false;
}
replace = false;
// If new enemy is more threatening then replace it reguardless if we can see it
if ( CalculateEnemyThreat ( replacement ) > CalculateEnemyThreat ( enemy.ent ) ) {
replace = true;
// Replace our enemy if we havent seen ours in a bit
} else if ( !IsEnemyRecentlyVisible ( 0.25f ) ) {
replace = true;
}
// Replace enemy?
if ( replace ) {
SetEnemy ( replacement );
}
return replace;
}
/*
============
idAI::UpdateThreat
============
*/
void idAI::UpdateThreat ( void ) {
// Start threat at base threat level
combat.threatCurrent = combat.threatBase;
// Adjust threat using current tactical state
switch ( combat.tacticalCurrent ) {
case AITACTICAL_HIDE: combat.threatCurrent *= 0.5f; break;
case AITACTICAL_MELEE: combat.threatCurrent *= 2.0f; break;
}
// Signifigantly reduced threat when in undying mode
if ( aifl.undying ) {
combat.threatCurrent *= 0.25f;
}
}
/*
============
idAI::CalculateEnemyThreat
============
*/
float idAI::CalculateEnemyThreat ( idEntity* enemyEnt ) {
// Calculate the adjusted threat for this actor
float threat = 1.0f;
if ( enemyEnt->IsType ( idAI::GetClassType ( ) ) ) {
idAI* enemyAI = static_cast<idAI*>(enemyEnt);
threat = enemyAI->combat.threatCurrent;
// Increase threat for enemies that are targetting us
if ( enemyAI->enemy.ent == this && enemyAI->combat.tacticalCurrent == AITACTICAL_MELEE ) {
threat *= 2.0f;
}
} else if ( enemyEnt->IsType ( idPlayer::GetClassType ( ) ) ) {
threat = 2.0f;
} else {
threat = 1.0f;
}
float enemyRangeSqr;
float distSqr;
enemyRangeSqr = (enemy.ent) ? Square ( enemy.range ) : Square ( combat.attackRange[1] );
distSqr = (physicsObj.GetOrigin ( ) - enemyEnt->GetPhysics()->GetOrigin ( )).LengthSqr ( );
if ( distSqr > 0 ) {
return threat * (enemyRangeSqr / distSqr);
}
return threat;
}
/*
============
idAI::CheckBlink
============
*/
void idAI::CheckBlink ( void ) {
// if ( IsSpeaking ( ) ) {
// return;
// }
idActor::CheckBlink ( );
}
/*
============
idAI::Speak
============
*/
bool idAI::Speak( const char *lipsync, bool random ){
assert( idStr::Icmpn( lipsync, "lipsync_", 7 ) == 0 );
if ( random ) {
// If there is no lipsync then skip it
if ( spawnArgs.MatchPrefix ( lipsync ) ) {
lipsync = spawnArgs.RandomPrefix ( lipsync, gameLocal.random );
} else {
lipsync = NULL;
}
} else {
lipsync = spawnArgs.GetString ( lipsync );
}
if ( !lipsync || !*lipsync ) {
return false;
}
if ( head ) {
speakTime = head->StartLipSyncing( lipsync );
} else {
speakTime = 0;
StartSoundShader (declManager->FindSound ( lipsync ), SND_CHANNEL_VOICE, SSF_IS_VO, false, &speakTime );
}
speakTime += gameLocal.time;
return true;
}
/*
============
idAI::StopSpeaking
============
*/
void idAI::StopSpeaking( bool stopAnims ){
speakTime = 0;
StopSound( SND_CHANNEL_VOICE, false );
if ( head.GetEntity() ) {
head.GetEntity()->StopSound( SND_CHANNEL_VOICE, false );
if ( stopAnims ) {
head.GetEntity()->GetAnimator()->ClearAllAnims( gameLocal.time, 100 );
}
}
}
/*
============
idAI::CanHitEnemyFromAnim
============
*/
bool idAI::CanHitEnemyFromAnim( int animNum, idVec3 offset ) {
idVec3 dir;
idVec3 local_dir;
idVec3 fromPos;
idMat3 axis;
idVec3 start;
trace_t tr;
idEntity* enemyEnt;
// Need an enemy.
if ( !enemy.ent ) {
return false;
}
// Enemy actor pointer
enemyEnt = static_cast<idEntity*>(enemy.ent.GetEntity());
// just do a ray test if close enough
if ( enemyEnt->GetPhysics()->GetAbsBounds().IntersectsBounds( physicsObj.GetAbsBounds().Expand( 16.0f ) ) ) {
return CanHitEnemy();
}
// calculate the world transform of the launch position
idVec3 org = physicsObj.GetOrigin()+offset;
idVec3 from;
dir = enemy.lastVisibleChestPosition - org;
physicsObj.GetGravityAxis().ProjectVector( dir, local_dir );
local_dir.z = 0.0f;
local_dir.ToVec2().Normalize();
axis = local_dir.ToMat3();
from = org + attackAnimInfo[ animNum ].attackOffset * axis;
/*
if( DebugFilter(ai_debugTactical) ) {
gameRenderWorld->DebugLine ( colorYellow, org + attackAnimInfo[ animNum ].eyeOffset * viewAxis, from, 5000 );
gameRenderWorld->DebugLine ( colorOrange, from, enemy.lastVisibleEyePosition, 5000 );
}
*/
// If the point we are shooting from is within our bounds then we are good to go, otherwise make sure its not in a wall
const idBounds &ownerBounds = physicsObj.GetAbsBounds();
if ( !ownerBounds.ContainsPoint ( from ) ) {
trace_t tr;
if ( !g_perfTest_aiNoVisTrace.GetBool() ) {
gameLocal.TracePoint( this, tr, org + attackAnimInfo[ animNum ].eyeOffset * axis, from, MASK_SHOT_BOUNDINGBOX, this );
if ( tr.fraction < 1.0f ) {
return false;
}
}
}
return CanSeeFrom ( from, enemy.lastVisibleEyePosition, true );
}
/*
================
idAI::ScriptedBegin
================
*/
bool idAI::ScriptedBegin ( bool endWithIdle, bool allowDormant ) {
if ( aifl.dead ) {
return false;
}
// Wakeup if not awake already
WakeUp ( );
aifl.scriptedEndWithIdle = endWithIdle;
aifl.scripted = true;
// combat.fl.aware = false;
combat.tacticalCurrent = AITACTICAL_NONE;
// Make sure the entity never goes dormant during a scripted event or
// the event may never end.
aifl.scriptedNeverDormant = !allowDormant;
dormantStart = 0;
/*
// actors will ignore enemies during scripted events
ClearEnemy ( );
*/
// Cancel any current movement
StopMove ( MOVE_STATUS_DONE );
move.fl.allowAnimMove = true;
return true;
}
/*
================
idAI::ScriptedEnd
================
*/
void idAI::ScriptedEnd ( void ) {
dormantStart = 0;
aifl.scripted = false;
}
/*
================
idAI::ScriptedStop
================
*/
void idAI::ScriptedStop ( void ) {
if ( !aifl.scripted ) {
return;
}
aifl.scriptedEndWithIdle = true;
aifl.scripted = false;
StopMove( MOVE_STATUS_DONE );
}
/*
================
idAI::ScriptedMove
================
*/
void idAI::ScriptedMove ( idEntity* destEnt, float minDist, bool endWithIdle ) {
if ( !ScriptedBegin ( endWithIdle ) ) {
return;
}
//disable all temporary blocked reachabilities due to teammate obstacle avoidance
aiManager.UnMarkAllReachBlocked();
//attempt the move - NOTE: this *can* fail if there's no route or AAS obstacles are in the way!
MoveToEntity ( destEnt, minDist );
//re-enable all temporary blocked reachabilities due to teammate obstacle avoidance
aiManager.ReMarkAllReachBlocked();
// Move the torso to the idle state if its not already there
SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
// Move the legs into the idle state so he will start moving
SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 );
// Set up state loop for moving
SetState ( "State_ScriptedMove" );
PostState ( "State_ScriptedStop" );
}
/*
================
idAI::ScriptedFace
================
*/
void idAI::ScriptedFace ( idEntity* faceEnt, bool endWithIdle ) {
if ( !ScriptedBegin ( endWithIdle ) ) {
return;
}
// Force idle while facing
SetAnimState ( ANIMCHANNEL_LEGS, "Legs_Idle", 4 );
SetAnimState ( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
// Start facing the entity
FaceEntity ( faceEnt );
SetState ( "State_ScriptedFace" );
PostState ( "State_ScriptedStop" );
}
/*
================
idAI::ScriptedAnim
Plays an the given animation in an un-interruptable state. If looping will continue indefinately until
another operation which will stop a scripted sequence is called. When done can optionally return the character
back to their normal processing if endWithIdle is set to true.
================
*/
void idAI::ScriptedAnim ( const char* animname, int blendFrames, bool loop, bool endWithIdle ) {
// Start the scripted sequence
if ( !ScriptedBegin ( endWithIdle, true ) ) {
return;
}
TurnToward ( move.current_yaw );
if ( loop ) {
// Loop the given animation
PlayCycle ( ANIMCHANNEL_TORSO, animname, blendFrames );
} else {
// Play the given animation
PlayAnim ( ANIMCHANNEL_TORSO, animname, blendFrames );
}
SetAnimState ( ANIMCHANNEL_LEGS, "Wait_Frame" );
SetAnimState ( ANIMCHANNEL_TORSO, "Torso_ScriptedAnim" );
DisableAnimState ( ANIMCHANNEL_LEGS );
SetState ( "Wait_ScriptedDone" );
PostState ( "State_ScriptedStop" );
}
/*
============
idAI::ScriptedPlaybackAim
============
*/
void idAI::ScriptedPlaybackAim ( const char* playback, int flags, int numFrames ) {
// Start the scripted sequence
if ( !ScriptedBegin ( false ) ) {
return;
}
mLookPlayback.Start( spawnArgs.GetString ( playback ), this, flags, numFrames );
// Wait till its done and mark it finished
SetState ( "State_ScriptedPlaybackAim" );
PostState ( "State_ScriptedStop" );
}
/*
============
idAI::ScriptedAction
============
*/
void idAI::ScriptedAction ( idEntity* actionEnt, bool endWithIdle ) {
const char* actionName;
if ( !actionEnt ) {
return;
}
// Get the action name
actionName = actionEnt->spawnArgs.GetString ( "action" );
if ( !*actionName ) {
gameLocal.Error ( "missing action keyword on scripted action entity '%s' for ai '%s'",
actionEnt->GetName(),
GetName() );
return;
}
// Start the scripted sequence
if ( !ScriptedBegin ( endWithIdle ) ) {
return;
}
scriptedActionEnt = actionEnt;
SetState ( "State_ScriptedStop" );
PerformAction ( va("TorsoAction_%s", actionName ), 4, true );
}
/*
============
idAI::FootStep
============
*/
void idAI::FootStep ( void ) {
idActor::FootStep ( );
ExecScriptFunction( funcs.footstep );
}
/*
============
idAI::SetScript
============
*/
void idAI::SetScript( const char* scriptName, const char* funcName ) {
if ( !funcName || !funcName[0] ) {
return;
}
// Set the associated script
if ( !idStr::Icmp ( scriptName, "first_sight" ) ) {
funcs.first_sight.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "sight" ) ) {
funcs.sight.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "pain" ) ) {
funcs.pain.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "damage" ) ) {
funcs.damage.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "death" ) ) {
funcs.death.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "attack" ) ) {
funcs.attack.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "init" ) ) {
funcs.init.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "onclick" ) ) {
funcs.onclick.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "launch_projectile" ) ) {
funcs.launch_projectile.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "footstep" ) ) {
funcs.footstep.Init( funcName );
} else if ( !idStr::Icmp ( scriptName, "postHeal" ) ) {
// hax: this is a medic only script and I don't want to store it on every AI type..
// I also don't want it generating the warning below in this case.
} else if ( !idStr::Icmp ( scriptName, "postWeaponDestroyed" ) ) {
// hax: this is a Gladiator/Light Tank only script and I don't want to store it on every AI type..
// I also don't want it generating the warning below in this case.
} else {
gameLocal.Warning ( "unknown script '%s' specified on entity '%s'", scriptName, name.c_str() );
}
}
/*
===============================================================================
idAI - Reactions
===============================================================================
*/
/*
============
idAI::ReactToShotAt
============
*/
void idAI::ReactToShotAt ( idEntity* attacker, const idVec3 &origOrigin, const idVec3 &origDir ) {
if ( g_perfTest_aiNoDodge.GetBool() ) {
return;
}
idVec3 foo;
idVec3 diff;
diff = GetPhysics()->GetOrigin() - origOrigin;
diff = origOrigin + diff.ProjectOntoVector ( origDir ) * origDir;
diff = diff - GetPhysics()->GetOrigin();
diff.NormalizeFast ( );
diff.z = 0;
idAngles angles = diff.ToAngles ( );
float angleDelta = idMath::AngleDelta ( angles[YAW], move.current_yaw );
combat.shotAtTime = gameLocal.time;
combat.shotAtAngle = angleDelta;
// Someone is attacking us so give them a chance to be our new enemy
CheckForReplaceEnemy ( attacker );
}
/*
============
idAI::ReactToPain
============
*/
void idAI::ReactToPain ( idEntity* attacker, int damage ) {
CheckForReplaceEnemy ( attacker );
}
/*
===============================================================================
idAI - Helpers
===============================================================================
*/
/*
============
idAI::UpdateHelper
============
*/
void idAI::UpdateHelper ( void ) {
rvAIHelper* oldhelper;
// Link ourselves to the closest helper
oldhelper = helperCurrent;
helperCurrent = aiManager.FindClosestHelper ( physicsObj.GetOrigin() );
// Ideal stays the same as current as long as it was the same when we started
if ( oldhelper == helperIdeal ) {
helperIdeal = helperCurrent;
}
}
/*
============
idAI::GetActiveHelper
When we have an enemy our current helper becomes the active helper, when we dont have an
enemy we instead use our ideal.
============
*/
rvAIHelper* idAI::GetActiveHelper ( void ) {
return GetEnemy ( ) ? helperCurrent : helperIdeal;
}
/*
===============================================================================
idAI - Tethers
===============================================================================
*/
/*
============
idAI::SetTether
============
*/
void idAI::SetTether ( rvAITether* newTether ) {
aifl.tetherMover = false;
// Clear our current tether?
if ( !newTether ) {
if ( tether ) {
tether = NULL;
ForceTacticalUpdate ( );
}
} else if ( newTether->IsType ( rvAITetherClear::GetClassType ( ) ) ) {
SetTether ( NULL );
} else {
if ( newTether && !newTether->ValidateAAS ( this ) ) {
// If you have aas error out to make them fix it
if ( aas ) {
gameLocal.Error ( "tether entity '%s' does no link into the aas for ai '%s'. (try moving it closer to the floor where the aas is)",
newTether->GetName(), GetName () );
// If we dont have aas, just warn
} else {
gameLocal.Warning ( "tether entity '%s' does no link into the aas for ai '%s'. (there is no aas available)",
newTether->GetName(), GetName () );
}
SetTether ( NULL );
} else if ( newTether != tether ) {
tether = newTether;
ForceTacticalUpdate ( );
}
}
}
/*
============
idAI::GetTether
============
*/
rvAITether* idAI::GetTether ( void ) {
return tether;
}
/*
============
idAI::IsTethered
============
*/
bool idAI::IsTethered ( void ) const {
// Need a tether entity to be tethered
if ( !tether ) {
return false;
}
// If we have an enemy and that enemy is within our tether then break it if we can
if ( enemy.ent && enemy.ent->IsType ( idAI::GetClassType() ) && tether->CanBreak ( ) ) {
if ( tether->ValidateDestination ( static_cast<idAI*>(enemy.ent.GetEntity()), enemy.lastKnownPosition ) ) {
return false;
}
}
return true;
}
/*
============
idAI::IsWithinTether
============
*/
bool idAI::IsWithinTether ( void ) const {
if ( !IsTethered ( ) ) {
return false;
}
if ( !tether->ValidateDestination ( (idAI*)this, physicsObj.GetOrigin ( ) ) ) {
return false;
}
return true;
}
/*
===============================================================================
idAI - NonCombat
===============================================================================
*/
/*
=====================
idAI::SetTalkState
=====================
*/
void idAI::SetTalkState ( talkState_t state ) {
// Make sure state is valid
if ( ( state < 0 ) || ( state >= NUM_TALK_STATES ) ) {
gameLocal.Error( "Invalid talk state (%d)", (int)state );
}
// Same state we are already in?
if ( talkState == state ) {
return;
}
// Set new talk state
talkState = state;
}
/*
============
idAI::SetPassivePrefix
============
*/
void idAI::SetPassivePrefix ( const char* prefix ) {
passive.prefix = prefix;
if ( passive.prefix.Length() ) {
passive.prefix += "_";
}
// Force an idle change
passive.idleAnimChangeTime = 0;
passive.fidgetTime = 0;
passive.talkTime = 0;
// Get animation prefixs
passive.fl.multipleIdles = GetPassiveAnimPrefix ( "idle", passive.animIdlePrefix );
GetPassiveAnimPrefix ( "fidget", passive.animFidgetPrefix );
GetPassiveAnimPrefix ( "talk", passive.animTalkPrefix );
}
/*
============
idAI::GetPassiveAnimPrefix
============
*/
bool idAI::GetPassiveAnimPrefix ( const char* animName, idStr& animPrefix ) {
const idKeyValue* key;
// First see if we have custom idle animations for the passive prefix
key = NULL;
if ( passive.prefix.Length ( ) ) {
animPrefix = va("anim_%s%s", passive.prefix.c_str(), animName );
key = spawnArgs.MatchPrefix ( animPrefix );
}
// If there are no custom idle animations for the prefix then see if there are any custom anims at all
if ( !key ) {
animPrefix = va("anim_%s", animName );
key = spawnArgs.MatchPrefix ( animPrefix );
}
if ( !key ) {
animPrefix = "";
return false;
}
return spawnArgs.MatchPrefix ( animPrefix, key ) ? true : false;
}
/*
===================
idAI::IsMeleeNeeded
===================
*/
bool idAI::IsMeleeNeeded( void ) {
if( enemy.ent && enemy.ent->IsType ( idAI::Type )) {
idAI* enemyAI = static_cast<idAI*>(enemy.ent.GetEntity());
//if our enemy is closing in on us and demands melee, we'll meet him.
if ( enemyAI->combat.tacticalCurrent == AITACTICAL_MELEE && enemy.range < combat.meleeRange ) {
return true;
}
//other checks...
}
return false;
}
/*
===================
idAI::IsCrouching
===================
*/
bool idAI::IsCrouching( void ) const {
return move.fl.crouching;
}
bool idAI::CheckDeathCausesMissionFailure( void )
{
if ( spawnArgs.GetString( "objectivetitle_failed", NULL ) )
{
return true;
}
if ( targets.Num() )
{
//go through my targets and see if any are of class rvObjectiveFailed
idEntity* targEnt;
for( int i = 0; i < targets.Num(); i++ ) {
targEnt = targets[ i ].GetEntity();
if ( !targEnt )
{
continue;
}
if ( !targEnt->IsType( rvObjectiveFailed::GetClassType() ) ) {
continue;
}
if ( !spawnArgs.GetString( "inv_objective", NULL ) ) {
continue;
}
//yep!
return true;
}
}
return false;
}