#include "../../idlib/precompiled.h" #pragma hdrstop #include "../Game_local.h" /* =============================================================================== rvAIActionTimer =============================================================================== */ /* ================ rvAIActionTimer::rvAIActionTimer ================ */ rvAIActionTimer::rvAIActionTimer ( void ) { time = 0; } /* ================ rvAIActionTimer::Init ================ */ bool rvAIActionTimer::Init ( const idDict& args, const char* name ) { rate = SEC2MS ( args.GetFloat ( va("%s_rate",name), "0" ) ); return true; } /* ================ rvAIActionTimer::Save ================ */ void rvAIActionTimer::Save ( idSaveGame *savefile ) const { savefile->WriteInt ( rate ); savefile->WriteInt ( time ); } /* ================ rvAIActionTimer::Restore ================ */ void rvAIActionTimer::Restore ( idRestoreGame *savefile ) { savefile->ReadInt ( rate ); savefile->ReadInt ( time ); } /* ================ rvAIActionTimer::Reset ================ */ void rvAIActionTimer::Reset ( int currentTime, float diversity, float scale ) { float _rate = rate * scale; time = currentTime + (-gameLocal.random.RandomInt( 2.0f * _rate * diversity ) + _rate * diversity + _rate); } /* ================ rvAIActionTimer::Clear ================ */ void rvAIActionTimer::Clear ( int currentTime ) { time = currentTime; } /* ================ rvAIActionTimer::Add ================ */ void rvAIActionTimer::Add ( int _time, float diversity ) { time += (-gameLocal.random.RandomInt( 2.0f * _time * diversity ) + _time * diversity + _time); } /* =============================================================================== rvAIAction =============================================================================== */ /* ================ rvAIAction::rvAIAction ================ */ rvAIAction::rvAIAction ( void ) { memset ( &fl, 0, sizeof(fl) ); status = STATUS_UNUSED; } /* ================ rvAIAction::Init ================ */ bool rvAIAction::Init ( const idDict& args, const char* name, const char* defaultState, int _flags ) { const idKeyValue* kv; if ( _flags & AIACTIONF_ATTACK ) { fl.isAttack = true; } if ( _flags & AIACTIONF_MELEE ) { fl.isMelee = true; } // Initialize timer timer.Init ( args, name ); // Is this action enabled? fl.disabled = !args.GetBool ( va("%s",name), "0" ); fl.noPain = args.GetBool ( va("%s_nopain",name), "0" ); fl.noTurn = args.GetBool ( va("%s_noturn",name), "1" ); fl.overrideLegs = args.GetBool ( va("%s_overrideLegs",name), "1" ); fl.noSimpleThink = args.GetBool ( va("%s_nosimplethink",name), "0" ); blendFrames = args.GetInt ( va("%s_blendFrames",name), "4" ); failRate = SEC2MS ( args.GetInt ( va("%s_failRate",name), ".1" ) ); minRange = args.GetInt ( va("%s_minRange",name), "0" ); maxRange = args.GetInt ( va("%s_maxRange",name), (_flags & AIACTIONF_ATTACK ) ? "-1" : "0" ); minRange2d = args.GetInt ( va("%s_minRange2d",name), "0" ); maxRange2d = args.GetInt ( va("%s_maxRange2d",name), "0" ); chance = args.GetFloat ( va("%s_chance",name), "1" ); diversity = args.GetFloat ( va("%s_diversity", name ), ".5" ); // action state state = args.GetString ( va("%s_state",name), (!defaultState||!*defaultState) ? "Torso_Action" : defaultState ); // allow for multiple animations const char* prefix = va("%s_anim",name); for ( kv = args.MatchPrefix ( prefix, NULL ); kv; kv = args.MatchPrefix ( prefix, kv ) ) { if ( kv->GetValue ( ).Length ( ) ) { anims.Append ( kv->GetValue ( ) ); } } return true; } /* ================ rvAIAction::Save ================ */ void rvAIAction::Save ( idSaveGame *savefile ) const { int i; savefile->Write( &fl, sizeof( fl ) ); savefile->WriteInt ( anims.Num ( ) ); for ( i = 0; i < anims.Num(); i ++ ) { savefile->WriteString ( anims[i] ); } savefile->WriteString ( state ); timer.Save ( savefile ); savefile->WriteInt ( blendFrames ); savefile->WriteInt ( failRate ); savefile->WriteFloat ( minRange ); savefile->WriteFloat ( maxRange ); savefile->WriteFloat ( minRange2d ); savefile->WriteFloat ( maxRange2d ); savefile->WriteFloat ( chance ); savefile->WriteFloat ( diversity ); savefile->WriteInt ( (int)status ); } /* ================ rvAIAction::Restore ================ */ void rvAIAction::Restore ( idRestoreGame *savefile ) { int num; savefile->Read( &fl, sizeof( fl ) ); savefile->ReadInt ( num ); anims.Clear ( ); anims.SetNum ( num ); for ( num--; num >= 0; num -- ) { savefile->ReadString ( anims[num] ); } savefile->ReadString ( state ); timer.Restore ( savefile ); savefile->ReadInt ( blendFrames ); savefile->ReadInt ( failRate ); savefile->ReadFloat ( minRange ); savefile->ReadFloat ( maxRange ); savefile->ReadFloat ( minRange2d ); savefile->ReadFloat ( maxRange2d ); savefile->ReadFloat ( chance ); savefile->ReadFloat ( diversity ); savefile->ReadInt ( (int&)status ); } /* =============================================================================== Actions =============================================================================== */ /* ================ idAI::CheckAction_EvadeLeft ================ */ bool idAI::CheckAction_EvadeLeft ( rvAIAction* action, int animNum ) { if( combat.shotAtAngle >= 0 || gameLocal.time - combat.shotAtTime > 100 ) { return false; } // TODO: dont evade unless it was coming from directly in front of us if ( animNum != -1 && !TestAnimMove ( animNum ) ) { return false; } return true; } /* ================ idAI::CheckAction_EvadeRight ================ */ bool idAI::CheckAction_EvadeRight ( rvAIAction* action, int animNum ) { if( combat.shotAtAngle < 0 || gameLocal.time - combat.shotAtTime > 100 ){ return false; } // TODO: Dont eveade unless it was coming from directly in front of us if ( animNum != -1 && !TestAnimMove ( animNum ) ) { return false; } return true; } /* ================ idAI::CheckAction_JumpBack ================ */ bool idAI::CheckAction_JumpBack ( rvAIAction* action, int animNum ) { // Jump back after taking damage if ( !aifl.damage ) { return false; } // TODO: enemy must be in front to jump backwards // Can we actually move backwards? if ( !TestAnimMove ( animNum ) ) { return false; } return true; } /* ================ idAI::CheckAction_RangedAttack ================ */ bool idAI::CheckAction_RangedAttack ( rvAIAction* action, int animNum ) { if ( !enemy.ent || !enemy.fl.inFov ) { return false; } if ( !IsEnemyRecentlyVisible ( ) || enemy.ent->DistanceTo ( enemy.lastKnownPosition ) > 128.0f ) { return false; } if ( animNum != -1 && !CanHitEnemyFromAnim( animNum ) ) { return false; } return true; } /* ================ idAI::CheckAction_MeleeAttack ================ */ bool idAI::CheckAction_MeleeAttack ( rvAIAction* action, int animNum ) { if ( !enemy.ent || !enemy.fl.inFov ) { return false; } if ( !CheckFOV ( enemy.ent->GetPhysics()->GetOrigin(), 10 ) ) { return false; } return true; } /* ================ idAI::CheckAction_LeapAttack ================ */ bool idAI::CheckAction_LeapAttack ( rvAIAction* action, int animNum ) { if ( !enemy.ent || !enemy.fl.inFov ) { return false; } if ( enemy.range > 64.0f && !TestAnimMove ( animNum, enemy.ent ) ) { return false; } // Must be looking right at the enemy to leap if ( !CheckFOV ( enemy.ent->GetPhysics()->GetOrigin(), 4 ) ) { return false; } return true; } /* ================ idAI::UpdateAction ================ */ bool idAI::UpdateAction ( void ) { // Update action MUST be called from the main state loop assert ( stateThread.IsExecuting ( ) ); // If an action is already running then dont let another start if ( aifl.action ) { return false; } return CheckActions ( ); } /* ================ idAI::CheckPainActions ================ */ bool idAI::CheckPainActions ( void ) { if ( !pain.takenThisFrame || !actionTimerPain.IsDone ( actionTime ) ) { return false; } if ( !pain.threshold || pain.takenThisFrame < pain.threshold ) { return false; } PerformAction ( "Torso_Pain", 2, true ); actionTimerPain.Reset ( actionTime ); return true; } /* ================ idAI::CheckActions ================ */ bool idAI::CheckActions ( void ) { // Pain? if ( CheckPainActions ( ) ) { return true; } // Actions are limited at a cover position to shooting and leaning if ( IsBehindCover ( ) ) { // Test ranged attack first if ( PerformAction ( &actionRangedAttack, (checkAction_t)&idAI::CheckAction_RangedAttack, &actionTimerRangedAttack ) ) { return true; } } else { if ( PerformAction ( &actionEvadeLeft, (checkAction_t)&idAI::CheckAction_EvadeLeft, &actionTimerEvade ) || PerformAction ( &actionEvadeRight, (checkAction_t)&idAI::CheckAction_EvadeRight, &actionTimerEvade ) || PerformAction ( &actionJumpBack, (checkAction_t)&idAI::CheckAction_JumpBack, &actionTimerEvade ) || PerformAction ( &actionLeapAttack, (checkAction_t)&idAI::CheckAction_LeapAttack ) ) { return true; } else if ( PerformAction ( &actionRangedAttack,(checkAction_t)&idAI::CheckAction_RangedAttack, &actionTimerRangedAttack ) || PerformAction ( &actionMeleeAttack, (checkAction_t)&idAI::CheckAction_MeleeAttack ) ) { return true; } } return false; } /* ================ idAI::PerformAction ================ */ void idAI::PerformAction ( const char* stateName, int blendFrames, bool noPain ) { // Allow movement in actions move.fl.allowAnimMove = true; // Start the action if ( legsAnim.Disabled() ) { //MCG: Hmmm... I hope this doesn't break anything, but if an action happens *right* // at the end of a trigger_anim, then the legs will be enabled (by the SetAnimState // on the torso) with no state! The actor will then be stuck in place until // something actually sets the legsAnim state... so let's check for disabled and // set a default state right here...? SetAnimState ( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); } SetAnimState ( ANIMCHANNEL_TORSO, stateName, blendFrames ); // Always call finish action when the action is done, it will clear the action flag aifl.action = true; PostAnimState ( ANIMCHANNEL_TORSO, "Torso_FinishAction", 0, 0, SFLAG_ONCLEAR ); // Go back to idle when done PostAnimState ( ANIMCHANNEL_TORSO, "Torso_Idle", blendFrames ); // Main state will wait until action is finished before continuing if ( noPain ) { InterruptState ( "Wait_ActionNoPain" ); } else { InterruptState ( "Wait_Action" ); } OnStartAction ( ); } bool idAI::PerformAction ( rvAIAction* action, bool (idAI::*condition)(rvAIAction*,int), rvAIActionTimer* timer ) { // If we arent ignoring simple think then dont perform this action on a simple think frame if ( !action->fl.noSimpleThink && aifl.simpleThink ) { return false; } // Is the action disabled? if ( action->fl.disabled ) { action->status = rvAIAction::STATUS_FAIL_DISABLED; return false; } // Action timers still running? if ( !action->timer.IsDone ( actionTime ) ) { action->status = rvAIAction::STATUS_FAIL_TIMER; return false; } if ( timer && !timer->IsDone ( actionTime ) ) { action->status = rvAIAction::STATUS_FAIL_EXTERNALTIMER; return false; } // Special code for attacks if ( action->fl.isAttack ) { // Attacks disabled? if ( ai_disableAttacks.GetBool() ) { action->status = rvAIAction::STATUS_FAIL_DISABLED; return false; } // No attack actions if we have no enemy or our enemy cant be hurt if ( !enemy.ent || enemy.ent->health <= 0 ) { action->status = rvAIAction::STATUS_FAIL_NOENEMY; return false; } } // Min Range check if ( action->minRange ) { if ( !enemy.ent || !enemy.range || enemy.range < action->minRange ) { action->status = rvAIAction::STATUS_FAIL_MINRANGE; return false; } } if ( action->minRange2d ) { if ( !enemy.ent || !enemy.range2d || enemy.range2d < action->minRange2d ) { action->status = rvAIAction::STATUS_FAIL_MINRANGE; return false; } } // Max Range check if ( action->maxRange != 0 ) { float maxrange = action->maxRange == -1 ? combat.attackRange[1] : action->maxRange; if ( !enemy.ent || !enemy.range || enemy.range > maxrange ) { if ( action->fl.isMelee && GetEnemy() ) { //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 = GetEnemy()->GetPhysics()->GetOrigin(); idBounds enemyBounds = GetEnemy()->GetPhysics()->GetBounds(); enemyBounds.TranslateSelf( enemyOrg ); if ( !bounds.IntersectsBounds( enemyBounds ) ) { action->status = rvAIAction::STATUS_FAIL_MAXRANGE; return false; } } else { action->status = rvAIAction::STATUS_FAIL_MAXRANGE; return false; } } } if ( action->maxRange2d ) { if ( !enemy.ent || !enemy.range2d || enemy.range2d > action->maxRange2d ) { action->status = rvAIAction::STATUS_FAIL_MAXRANGE; return false; } } int animNum; if ( action->anims.Num ( ) ) { // Pick a random animation from the list animNum = GetAnim ( ANIMCHANNEL_TORSO, action->anims[gameLocal.random.RandomInt(action->anims.Num())] ); if ( !animNum ) { action->status = rvAIAction::STATUS_FAIL_ANIM; return false; } } else { animNum = -1; } // Random chance? if ( action->chance < 1.0f && gameLocal.random.RandomFloat ( ) > action->chance ) { action->status = rvAIAction::STATUS_FAIL_CHANCE; action->timer.Clear ( actionTime ); action->timer.Add ( 100 ); return false; } // Check the condition if ( condition && !(this->*(condition)) ( action, animNum ) ) { action->status = rvAIAction::STATUS_FAIL_CONDITION; action->timer.Clear ( actionTime ); action->timer.Add ( action->failRate ); return false; } // Disallow turning during action? if ( action->fl.noTurn ) { OverrideFlag ( AIFLAGOVERRIDE_NOTURN, true ); } // Perform the raw action PerformAction ( action->state, action->blendFrames, action->fl.noPain ); // Override legs for this state? if ( action->fl.overrideLegs ) { DisableAnimState ( ANIMCHANNEL_LEGS ); } // When attacking scale the time by the aggression scale float scale; if ( action->fl.isAttack ) { scale = 2.0f - combat.aggressiveScale; } else { scale = 1.0f; } // Restart the action timer using the length of the animation being played action->timer.Reset ( actionTime, action->diversity, scale ); // If the action gets interrupted it will be skipped, if it is then move the action timers forward // by half of its normal delay to allow it to be performed again quicker than usual. This also allows // other actions that may be still pending to be performed and thus cause less of a pause after taking pain. actionSkipTime = (action->timer.GetTime ( ) + actionTime) / 2; // Restart the global action timer using the length of the animation being played if ( timer ) { timer->Reset ( actionTime, action->diversity, scale ); } action->status = rvAIAction::STATUS_OK; actionAnimNum = animNum; return true; }