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

448 lines
14 KiB
C++

#include "../../idlib/precompiled.h"
#pragma hdrstop
#include "../Game_local.h"
class rvMonsterGunner : public idAI {
public:
CLASS_PROTOTYPE( rvMonsterGunner );
rvMonsterGunner ( void );
void InitSpawnArgsVariables ( void );
void Spawn ( void );
void Save ( idSaveGame *savefile ) const;
void Restore ( idRestoreGame *savefile );
// Add some dynamic externals for debugging
virtual void GetDebugInfo ( debugInfoProc_t proc, void* userData );
virtual bool Pain ( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location );
protected:
int shots;
int shotsFired;
idStr nailgunPrefix;
int nailgunMinShots;
int nailgunMaxShots;
int nextShootTime;
int attackRate;
jointHandle_t attackJoint;
virtual void OnStopMoving ( aiMoveCommand_t oldMoveCommand );
virtual bool UpdateRunStatus ( void );
virtual int FilterTactical ( int availableTactical );
virtual bool CheckActions ( void );
virtual void OnTacticalChange ( aiTactical_t oldTactical );
private:
// Actions
rvAIAction actionGrenadeAttack;
rvAIAction actionNailgunAttack;
rvAIAction actionSideStepLeft;
rvAIAction actionSideStepRight;
rvAIActionTimer actionTimerSideStep;
bool CheckAction_SideStepLeft ( rvAIAction* action, int animNum );
bool CheckAction_SideStepRight ( rvAIAction* action, int animNum );
// Torso States
stateResult_t State_Torso_NailgunAttack ( const stateParms_t& parms );
stateResult_t State_Torso_MovingRangedAttack ( const stateParms_t& parms );
CLASS_STATES_PROTOTYPE ( rvMonsterGunner );
};
CLASS_DECLARATION( idAI, rvMonsterGunner )
END_CLASS
/*
================
rvMonsterGunner::rvMonsterGunner
================
*/
rvMonsterGunner::rvMonsterGunner ( ) {
nextShootTime = 0;
}
void rvMonsterGunner::InitSpawnArgsVariables( void )
{
nailgunMinShots = spawnArgs.GetInt ( "action_nailgunAttack_minshots", "5" );
nailgunMaxShots = spawnArgs.GetInt ( "action_nailgunAttack_maxshots", "20" );
attackRate = SEC2MS( spawnArgs.GetFloat( "attackRate", "0.3" ) );
attackJoint = animator.GetJointHandle( spawnArgs.GetString( "attackJoint", "muzzle" ) );
}
/*
================
rvMonsterGunner::Spawn
================
*/
void rvMonsterGunner::Spawn ( void ) {
actionGrenadeAttack.Init ( spawnArgs, "action_grenadeAttack", NULL, AIACTIONF_ATTACK );
actionNailgunAttack.Init ( spawnArgs, "action_nailgunAttack", "Torso_NailgunAttack", AIACTIONF_ATTACK );
actionSideStepLeft.Init ( spawnArgs, "action_sideStepLeft", NULL, 0 );
actionSideStepRight.Init ( spawnArgs, "action_sideStepRight", NULL, 0 );
actionTimerSideStep.Init ( spawnArgs, "actionTimer_sideStep" );
InitSpawnArgsVariables();
}
/*
================
rvMonsterGunner::Save
================
*/
void rvMonsterGunner::Save ( idSaveGame *savefile ) const {
actionGrenadeAttack.Save ( savefile );
actionNailgunAttack.Save ( savefile );
actionSideStepLeft.Save ( savefile );
actionSideStepRight.Save ( savefile );
actionTimerSideStep.Save ( savefile );
savefile->WriteInt ( shots );
savefile->WriteInt ( shotsFired );
savefile->WriteString ( nailgunPrefix );
savefile->WriteInt ( nextShootTime );
}
/*
================
rvMonsterGunner::Restore
================
*/
void rvMonsterGunner::Restore ( idRestoreGame *savefile ) {
actionGrenadeAttack.Restore ( savefile );
actionNailgunAttack.Restore ( savefile );
actionSideStepLeft.Restore ( savefile );
actionSideStepRight.Restore ( savefile );
actionTimerSideStep.Restore ( savefile );
savefile->ReadInt ( shots );
savefile->ReadInt ( shotsFired );
savefile->ReadString ( nailgunPrefix );
savefile->ReadInt ( nextShootTime );
InitSpawnArgsVariables();
}
/*
============
rvMonsterGunner::OnStopMoving
============
*/
void rvMonsterGunner::OnStopMoving ( aiMoveCommand_t oldMoveCommand ) {
//MCG - once you get to your position, attack immediately (no pause)
//FIXME: Restrict this some? Not after animmoves? Not if move was short? Only in certain tactical states?
if ( GetEnemy() )
{
if ( combat.tacticalCurrent == AITACTICAL_HIDE )
{//hiding
}
else if ( combat.tacticalCurrent == AITACTICAL_MELEE || enemy.range <= combat.meleeRange )
{//in melee state or in melee range
actionMeleeAttack.timer.Clear( actionTime );
}
else if ( (!actionNailgunAttack.timer.IsDone(actionTime) || !actionTimerRangedAttack.IsDone(actionTime))
&& (!actionGrenadeAttack.timer.IsDone(actionTime) || !actionTimerSpecialAttack.IsDone(actionTime)) )
{//no attack is ready
//Ready at least one of them
if ( gameLocal.random.RandomInt(3) )
{
actionNailgunAttack.timer.Clear( actionTime );
actionTimerRangedAttack.Clear( actionTime );
}
else
{
actionGrenadeAttack.timer.Clear( actionTime );
actionTimerSpecialAttack.Clear( actionTime );
}
}
}
}
/*
================
rvMonsterGunner::UpdateRunStatus
================
*/
bool rvMonsterGunner::UpdateRunStatus ( void ) {
move.fl.idealRunning = false;
return move.fl.running != move.fl.idealRunning;
}
/*
================
rvMonsterGunner::FilterTactical
================
*/
int rvMonsterGunner::FilterTactical ( int availableTactical ) {
if ( !move.fl.moving && enemy.range > combat.meleeRange )
{//keep moving!
if ( (!actionNailgunAttack.timer.IsDone(actionTime+500) || !actionTimerRangedAttack.IsDone(actionTime+500))
&& (!actionGrenadeAttack.timer.IsDone(actionTime+500) || !actionTimerSpecialAttack.IsDone(actionTime+500)) )
{//won't be attacking in the next 1 second
combat.tacticalUpdateTime = 0;
availableTactical |= (AITACTICAL_MELEE_BIT);
if ( !gameLocal.random.RandomInt(2) )
{
availableTactical &= ~(AITACTICAL_RANGED_BITS);
}
}
}
return idAI::FilterTactical ( availableTactical );
}
/*
================
rvMonsterGunner::OnTacticalChange
Enable/Disable the ranged attack based on whether the grunt needs it
================
*/
void rvMonsterGunner::OnTacticalChange ( aiTactical_t oldTactical ) {
switch ( combat.tacticalCurrent ) {
case AITACTICAL_MELEE:
//walk for at least 2 seconds (default update time of 500 is too short)
combat.tacticalUpdateTime = gameLocal.GetTime() + 2000 + gameLocal.random.RandomInt(1000);
break;
}
}
/*
================
rvMonsterGunner::CheckAction_SideStepLeft
================
*/
bool rvMonsterGunner::CheckAction_SideStepLeft ( rvAIAction* action, int animNum ) {
if ( animNum == -1 ) {
return false;
}
idVec3 moveVec;
TestAnimMove( animNum, NULL, &moveVec );
//NOTE: should we care if we can't walk all the way to the left?
int attAnimNum = -1;
if ( actionNailgunAttack.anims.Num ( ) ) {
// Pick a random animation from the list
attAnimNum = GetAnim ( ANIMCHANNEL_TORSO, actionNailgunAttack.anims[gameLocal.random.RandomInt(actionNailgunAttack.anims.Num())] );
}
if ( attAnimNum != -1 && !CanHitEnemyFromAnim( attAnimNum, moveVec ) ) {
return false;
}
return true;
}
/*
================
rvMonsterGunner::CheckAction_SideStepRight
================
*/
bool rvMonsterGunner::CheckAction_SideStepRight ( rvAIAction* action, int animNum ) {
if ( animNum == -1 ) {
return false;
}
idVec3 moveVec;
TestAnimMove ( animNum, NULL, &moveVec );
//NOTE: should we care if we can't walk all the way to the right?
int attAnimNum = -1;
if ( actionNailgunAttack.anims.Num ( ) ) {
// Pick a random animation from the list
attAnimNum = GetAnim ( ANIMCHANNEL_TORSO, actionNailgunAttack.anims[gameLocal.random.RandomInt(actionNailgunAttack.anims.Num())] );
}
if ( attAnimNum != -1 && !CanHitEnemyFromAnim( attAnimNum, moveVec ) ) {
return false;
}
return true;
}
/*
================
rvMonsterGunner::CheckActions
================
*/
bool rvMonsterGunner::CheckActions ( void ) {
// Fire a grenade?
if ( PerformAction ( &actionGrenadeAttack, (checkAction_t)&idAI::CheckAction_RangedAttack, &actionTimerSpecialAttack ) ||
PerformAction ( &actionNailgunAttack, (checkAction_t)&idAI::CheckAction_RangedAttack, &actionTimerRangedAttack ) ) {
return true;
}
bool action = idAI::CheckActions( );
if ( !action ) {
//try a strafe
if ( GetEnemy() && enemy.fl.visible && gameLocal.GetTime()-lastAttackTime > actionTimerRangedAttack.GetRate()+1000 ) {
//we can see our enemy but haven't been able to shoot him in a while...
if ( PerformAction ( &actionSideStepLeft, (checkAction_t)&rvMonsterGunner::CheckAction_SideStepLeft, &actionTimerSideStep )
|| PerformAction ( &actionSideStepRight, (checkAction_t)&rvMonsterGunner::CheckAction_SideStepRight, &actionTimerSideStep ) ) {
return true;
}
}
}
return action;
}
/*
=====================
rvMonsterGunner::GetDebugInfo
=====================
*/
void rvMonsterGunner::GetDebugInfo ( debugInfoProc_t proc, void* userData ) {
// Base class first
idAI::GetDebugInfo ( proc, userData );
proc ( "idAI", "action_grenadeAttack", aiActionStatusString[actionGrenadeAttack.status], userData );
proc ( "idAI", "action_nailgunAttack", aiActionStatusString[actionNailgunAttack.status], userData );
proc ( "idAI", "actionSideStepLeft", aiActionStatusString[actionSideStepLeft.status], userData );
proc ( "idAI", "actionSideStepRight", aiActionStatusString[actionSideStepRight.status], userData );
}
bool rvMonsterGunner::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) {
actionTimerRangedAttack.Clear( actionTime );
actionNailgunAttack.timer.Clear( actionTime );
return (idAI::Pain( inflictor, attacker, damage, dir, location ));
}
/*
===============================================================================
States
===============================================================================
*/
CLASS_STATES_DECLARATION ( rvMonsterGunner )
STATE ( "Torso_NailgunAttack", rvMonsterGunner::State_Torso_NailgunAttack )
STATE ( "Torso_MovingRangedAttack", rvMonsterGunner::State_Torso_MovingRangedAttack )
END_CLASS_STATES
/*
================
rvMonsterGunner::State_Torso_MovingRangedAttack
================
*/
stateResult_t rvMonsterGunner::State_Torso_MovingRangedAttack ( const stateParms_t& parms ) {
enum {
STAGE_INIT,
STAGE_SHOOT,
STAGE_SHOOT_WAIT,
};
switch ( parms.stage ) {
case STAGE_INIT:
shots = (gameLocal.random.RandomInt ( nailgunMaxShots - nailgunMinShots ) + nailgunMinShots) * combat.aggressiveScale;
shotsFired = 0;
return SRESULT_STAGE ( STAGE_SHOOT );
case STAGE_SHOOT:
shots--;
shotsFired++;
nextShootTime = gameLocal.GetTime() + attackRate;
if ( attackJoint != INVALID_JOINT ) {
Attack( "nail", attackJoint, GetEnemy() );
PlayEffect( "fx_nail_flash", attackJoint );
}
StartSound( "snd_nailgun_fire", SND_CHANNEL_WEAPON, 0, false, 0 );
/*
switch ( move.currentDirection )
{
case MOVEDIR_RIGHT:
PlayAnim ( ANIMCHANNEL_TORSO, "range_attack_torso_right", 0 );
break;
case MOVEDIR_LEFT:
PlayAnim ( ANIMCHANNEL_TORSO, "range_attack_torso_left", 0 );
break;
case MOVEDIR_BACKWARD:
PlayAnim ( ANIMCHANNEL_TORSO, "range_attack_torso_back", 0 );
break;
default:
PlayAnim ( ANIMCHANNEL_TORSO, "range_attack_torso", 0 );
break;
}
*/
return SRESULT_STAGE ( STAGE_SHOOT_WAIT );
case STAGE_SHOOT_WAIT:
// When the shoot animation is done either play another shot animation
// or finish up with post_shooting
if ( gameLocal.GetTime() >= nextShootTime ) {
if ( shots <= 0 || (!enemy.fl.inFov && shotsFired >= nailgunMinShots) || !move.fl.moving ) {
return SRESULT_DONE;
}
return SRESULT_STAGE ( STAGE_SHOOT);
}
return SRESULT_WAIT;
}
return SRESULT_ERROR;
}
/*
================
rvMonsterGunner::State_Torso_NailgunAttack
================
*/
stateResult_t rvMonsterGunner::State_Torso_NailgunAttack ( const stateParms_t& parms ) {
static const char* nailgunAnims [ ] = { "nailgun_short", "nailgun_long" };
enum {
STAGE_INIT,
STAGE_WAITSTART,
STAGE_LOOP,
STAGE_WAITLOOP,
STAGE_WAITEND
};
switch ( parms.stage ) {
case STAGE_INIT:
// If moving switch to the moving ranged attack (torso only)
if ( move.fl.moving && !actionNailgunAttack.fl.overrideLegs && FacingIdeal() && !gameLocal.random.RandomInt(1) ) {
PostAnimState ( ANIMCHANNEL_TORSO, "Torso_MovingRangedAttack", parms.blendFrames );
return SRESULT_DONE;
}
shots = (gameLocal.random.RandomInt ( nailgunMaxShots - nailgunMinShots ) + nailgunMinShots) * combat.aggressiveScale;
DisableAnimState ( ANIMCHANNEL_LEGS );
shotsFired = 0;
nailgunPrefix = nailgunAnims[shots%2];
if ( !CanHitEnemyFromAnim( GetAnim( ANIMCHANNEL_TORSO, va("%s_loop", nailgunPrefix.c_str() ) ) ) )
{//this is hacky, but we really need to test the attack anim first since they're so different
//can't hit with this one, just use the other one...
nailgunPrefix = nailgunAnims[(shots+1)%2];
}
PlayAnim ( ANIMCHANNEL_TORSO, va("%s_start", nailgunPrefix.c_str() ), parms.blendFrames );
return SRESULT_STAGE ( STAGE_WAITSTART );
case STAGE_WAITSTART:
if ( AnimDone ( ANIMCHANNEL_TORSO, 0 ) ) {
return SRESULT_STAGE ( STAGE_LOOP );
}
return SRESULT_WAIT;
case STAGE_LOOP:
PlayAnim ( ANIMCHANNEL_TORSO, va("%s_loop", nailgunPrefix.c_str() ), 0 );
shotsFired++;
return SRESULT_STAGE ( STAGE_WAITLOOP );
case STAGE_WAITLOOP:
if ( AnimDone ( ANIMCHANNEL_TORSO, 0 ) ) {
if ( --shots <= 0 || (shotsFired >= nailgunMinShots && !enemy.fl.inFov) || aifl.damage ) {
PlayAnim ( ANIMCHANNEL_TORSO, va("%s_end", nailgunPrefix.c_str() ), 0 );
return SRESULT_STAGE ( STAGE_WAITEND );
}
return SRESULT_STAGE ( STAGE_LOOP );
}
return SRESULT_WAIT;
case STAGE_WAITEND:
if ( AnimDone ( ANIMCHANNEL_TORSO, 4 ) ) {
return SRESULT_DONE;
}
return SRESULT_WAIT;
}
return SRESULT_ERROR;
}