449 lines
14 KiB
C++
449 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;
|
||
|
}
|