/* ================ 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 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(&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 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( i ); talkTarget.Restore( savefile ); savefile->ReadInt( i ); talkMessage = static_cast( i ); savefile->ReadInt( talkBusyCount ); savefile->ReadInt ( speakTime ); // Focus lookTarget.Restore ( savefile ); savefile->ReadInt( i ); focusType = static_cast( 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(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(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( 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(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( ent ); if ( actor->IsType( idPlayer::GetClassType() ) && static_cast(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( 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(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( 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( 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(enemy.ent.GetEntity()); idEntity* enemyEnt = static_cast(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( 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(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(targetEnt)->GetEyePosition ( ); targetPos1 = static_cast(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(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(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( 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(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(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(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( 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(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(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( 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&&distSqrIsType ( idAI::Type ) ) { idAI* teammateAI = static_cast(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(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(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(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(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(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(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; }