diff --git a/neo/doom3/game/ai/AI_alphalabs2_scientist1.cpp b/neo/doom3/game/ai/AI_alphalabs2_scientist1.cpp new file mode 100644 index 00000000..44778a71 --- /dev/null +++ b/neo/doom3/game/ai/AI_alphalabs2_scientist1.cpp @@ -0,0 +1,506 @@ +// AI_alphalabs2_scientist1.cpp +// Native state-machine replacement for ai_alphalabs2_scientist1.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmAIObject, rvmAIAlphaLabs2Scientist1 ) +END_CLASS + +void rvmAIAlphaLabs2Scientist1::Init( idAI* aiOwner ) { + rvmAIObject::Init( aiOwner ); + SetState( "state_Begin" ); +} + +void rvmAIAlphaLabs2Scientist1::Destroy() { + rvmAIObject::Destroy(); +} + +stateResult_t rvmAIAlphaLabs2Scientist1::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Head_Idle", Head_Idle ); + AI_DISPATCH_STATE( "Head_Death", Head_Death ); + AI_DISPATCH_STATE( "Head_DeathScene", Head_DeathScene ); + AI_DISPATCH_STATE( "Head_Greeting", Head_Greeting ); + AI_DISPATCH_STATE( "Head_Speech", Head_Speech ); + AI_DISPATCH_STATE( "Head_Hurry", Head_Hurry ); + AI_DISPATCH_STATE( "Head_StayClose", Head_StayClose ); + AI_DISPATCH_STATE( "Head_Electro_Pulses", Head_Electro_Pulses ); + AI_DISPATCH_STATE( "Head_Pulse1", Head_Pulse1 ); + AI_DISPATCH_STATE( "Head_Pulse2", Head_Pulse2 ); + AI_DISPATCH_STATE( "Head_WatchIt", Head_WatchIt ); + AI_DISPATCH_STATE( "Head_Spooked", Head_Spooked ); + AI_DISPATCH_STATE( "Head_Exit", Head_Exit ); + AI_DISPATCH_STATE( "Head_SayStuff", Head_SayStuff ); + AI_DISPATCH_STATE( "Head_Attacked", Head_Attacked ); + AI_DISPATCH_STATE( "Head_AttackDone", Head_AttackDone ); + AI_DISPATCH_STATE( "Torso_Work", Torso_Work ); + AI_DISPATCH_STATE( "Torso_DeathScene", Torso_DeathScene ); + AI_DISPATCH_STATE( "Torso_Death", Torso_Death ); + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Legs_Death", Legs_Death ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_TurnLeft", Legs_TurnLeft ); + AI_DISPATCH_STATE( "Legs_TurnRight", Legs_TurnRight ); + AI_DISPATCH_STATE( "Legs_Cower", Legs_Cower ); + AI_DISPATCH_STATE( "light_fade1", light_fade1 ); + AI_DISPATCH_STATE( "light_fade2", light_fade2 ); + AI_DISPATCH_STATE( "doEvent", doEvent ); + AI_DISPATCH_STATE( "sayStuff", sayStuff ); + AI_DISPATCH_STATE( "hurry_up", hurry_up ); + AI_DISPATCH_STATE( "updatePlayerPositionThread", updatePlayerPositionThread ); + AI_DISPATCH_STATE( "checkBlocked", checkBlocked ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "state_DeathScene", state_DeathScene ); + AI_DISPATCH_STATE( "state_WorkingOnStuff", state_WorkingOnStuff ); + AI_DISPATCH_STATE( "state_NoticePlayer", state_NoticePlayer ); + AI_DISPATCH_STATE( "state_Wait", state_Wait ); + AI_DISPATCH_STATE( "state_GetCloser", state_GetCloser ); + AI_DISPATCH_STATE( "state_CantReachPlayer", state_CantReachPlayer ); + AI_DISPATCH_STATE( "state_Spooked", state_Spooked ); + AI_DISPATCH_STATE( "state_Lead", state_Lead ); + AI_DISPATCH_STATE( "state_FindPlayer", state_FindPlayer ); + AI_DISPATCH_STATE( "state_Cower", state_Cower ); + return rvmAIObject::RunStateByName( stateName, parms ); +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_HEAD, "stand" ); parms->stage = 1; } + return SRESULT_WAIT; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Death( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_DeathScene( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Greeting( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Speech( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Hurry( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_StayClose( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Electro_Pulses( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Pulse1( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Pulse2( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_WatchIt( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Spooked( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Exit( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_SayStuff( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_Attacked( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Head_AttackDone( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Torso_Work( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Torso_DeathScene( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Torso_Death( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { FinishAction( "pain" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Legs_Death( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "stand" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Legs_TurnLeft( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Legs_TurnRight( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::Legs_Cower( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::light_fade1( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::light_fade2( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::doEvent( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::sayStuff( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::hurry_up( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::updatePlayerPositionThread( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::checkBlocked( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_Killed( stateParms_t* parms ) { + StopMove(); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 0 ); + StopThinking(); + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_DeathScene( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_WorkingOnStuff( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_NoticePlayer( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_Wait( stateParms_t* parms ) { + if ( GetEnemy() ) { SetState( "state_Combat" ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_GetCloser( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_CantReachPlayer( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_Spooked( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_Lead( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_FindPlayer( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAIAlphaLabs2Scientist1::state_Cower( stateParms_t* parms ) { + return SRESULT_DONE; +} + +void rvmAIAlphaLabs2Scientist1::faceTowardsEntity( idEntity* target, float duration ) { +} + +void rvmAIAlphaLabs2Scientist1::light_fade_out( float fadeTime ) { +} + +void rvmAIAlphaLabs2Scientist1::light_fade_in( float fadeTime ) { +} + +void rvmAIAlphaLabs2Scientist1::light_fade( float stay_off ) { +} + +float rvmAIAlphaLabs2Scientist1::checkDestinationDistance( ) { + return 0.0f; +} diff --git a/neo/doom3/game/ai/AI_alphalabs2_scientist1.h b/neo/doom3/game/ai/AI_alphalabs2_scientist1.h new file mode 100644 index 00000000..b0627dba --- /dev/null +++ b/neo/doom3/game/ai/AI_alphalabs2_scientist1.h @@ -0,0 +1,67 @@ +// AI_alphalabs2_scientist1.h +// Native state-machine replacement for ai_alphalabs2_scientist1.script. + +#pragma once + + +class rvmAIAlphaLabs2Scientist1 : public rvmAIObject { +public: + CLASS_PROTOTYPE( rvmAIAlphaLabs2Scientist1 ); + + virtual void Init( idAI* aiOwner ) override; + virtual void Destroy() override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + virtual stateResult_t Head_Idle( stateParms_t* parms ); + virtual stateResult_t Head_Death( stateParms_t* parms ); + virtual stateResult_t Head_DeathScene( stateParms_t* parms ); + virtual stateResult_t Head_Greeting( stateParms_t* parms ); + virtual stateResult_t Head_Speech( stateParms_t* parms ); + virtual stateResult_t Head_Hurry( stateParms_t* parms ); + virtual stateResult_t Head_StayClose( stateParms_t* parms ); + virtual stateResult_t Head_Electro_Pulses( stateParms_t* parms ); + virtual stateResult_t Head_Pulse1( stateParms_t* parms ); + virtual stateResult_t Head_Pulse2( stateParms_t* parms ); + virtual stateResult_t Head_WatchIt( stateParms_t* parms ); + virtual stateResult_t Head_Spooked( stateParms_t* parms ); + virtual stateResult_t Head_Exit( stateParms_t* parms ); + virtual stateResult_t Head_SayStuff( stateParms_t* parms ); + virtual stateResult_t Head_Attacked( stateParms_t* parms ); + virtual stateResult_t Head_AttackDone( stateParms_t* parms ); + virtual stateResult_t Torso_Work( stateParms_t* parms ); + virtual stateResult_t Torso_DeathScene( stateParms_t* parms ); + virtual stateResult_t Torso_Death( stateParms_t* parms ); + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Legs_Death( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_TurnLeft( stateParms_t* parms ); + virtual stateResult_t Legs_TurnRight( stateParms_t* parms ); + virtual stateResult_t Legs_Cower( stateParms_t* parms ); + virtual stateResult_t light_fade1( stateParms_t* parms ); + virtual stateResult_t light_fade2( stateParms_t* parms ); + virtual stateResult_t doEvent( stateParms_t* parms ); + virtual stateResult_t sayStuff( stateParms_t* parms ); + virtual stateResult_t hurry_up( stateParms_t* parms ); + virtual stateResult_t updatePlayerPositionThread( stateParms_t* parms ); + virtual stateResult_t checkBlocked( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t state_DeathScene( stateParms_t* parms ); + virtual stateResult_t state_WorkingOnStuff( stateParms_t* parms ); + virtual stateResult_t state_NoticePlayer( stateParms_t* parms ); + virtual stateResult_t state_Wait( stateParms_t* parms ); + virtual stateResult_t state_GetCloser( stateParms_t* parms ); + virtual stateResult_t state_CantReachPlayer( stateParms_t* parms ); + virtual stateResult_t state_Spooked( stateParms_t* parms ); + virtual stateResult_t state_Lead( stateParms_t* parms ); + virtual stateResult_t state_FindPlayer( stateParms_t* parms ); + virtual stateResult_t state_Cower( stateParms_t* parms ); + + virtual void faceTowardsEntity( idEntity* target, float duration ); + virtual void light_fade_out( float fadeTime ); + virtual void light_fade_in( float fadeTime ); + virtual void light_fade( float stay_off ); + virtual float checkDestinationDistance( ); +}; diff --git a/neo/doom3/game/ai/AI_base.cpp b/neo/doom3/game/ai/AI_base.cpp new file mode 100644 index 00000000..9cc57a1f --- /dev/null +++ b/neo/doom3/game/ai/AI_base.cpp @@ -0,0 +1,874 @@ +// AI_base.cpp +// Native state-machine replacement for ai_base.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( idClass, rvmAIObject ) +END_CLASS + +rvmAIObject::rvmAIObject() { + owner = NULL; + scratchEndTime = 0.0f; + scratchMinTime = 0.0f; + scratchNextTime = 0.0f; + scratchCount = 0.0f; + scratchFlags = 0.0f; + scratchBool = false; + scratchEntity = NULL; + ClearParms( stateParms ); + ClearParms( actionParms ); + for ( int i = 0; i < AI_NATIVE_MAX_CHANNELS; ++i ) { + ClearParms( animParms[i] ); + } +} + +rvmAIObject::~rvmAIObject() { + Destroy(); +} + +void rvmAIObject::Init( idAI* aiOwner ) { + owner = aiOwner; + currentState.Clear(); + currentAction.Clear(); + waitActionName.Clear(); + for ( int i = 0; i < AI_NATIVE_MAX_CHANNELS; ++i ) { + currentAnimState[i].Clear(); + ClearParms( animParms[i] ); + } + ClearParms( stateParms ); + ClearParms( actionParms ); +} + +void rvmAIObject::Destroy() { + owner = NULL; + currentState.Clear(); + currentAction.Clear(); + waitActionName.Clear(); +} + +void rvmAIObject::Update() { + for ( int i = 0; i < AI_NATIVE_MAX_CHANNELS; ++i ) { + if ( currentAnimState[i].Length() ) { + RunStateByName( currentAnimState[i].c_str(), &animParms[i] ); + } + } + + if ( currentAction.Length() ) { + stateResult_t result = RunStateByName( currentAction.c_str(), &actionParms ); + if ( result != SRESULT_WAIT ) { + currentAction.Clear(); + ClearParms( actionParms ); + } + return; + } + + if ( currentState.Length() ) { + RunStateByName( currentState.c_str(), &stateParms ); + } +} + +void rvmAIObject::SetState( const char* stateName ) { + currentState = stateName ? stateName : ""; + ClearParms( stateParms ); +} + +void rvmAIObject::SetAnimState( int channel, const char* stateName, int blendFrames ) { + if ( channel == ANIMCHANNEL_ALL ) { + SetAnimState( ANIMCHANNEL_HEAD, stateName, blendFrames ); + SetAnimState( ANIMCHANNEL_TORSO, stateName, blendFrames ); + SetAnimState( ANIMCHANNEL_LEGS, stateName, blendFrames ); + return; + } + + const int index = ChannelIndex( channel ); + if ( index < 0 ) { + return; + } + + currentAnimState[index] = stateName ? stateName : ""; + ClearParms( animParms[index] ); + SetBlendFrames( channel, blendFrames ); + + if ( owner ) { + owner->Event_EnableAnim( channel, blendFrames ); + if ( channel == ANIMCHANNEL_TORSO ) { + owner->Event_EnableAnim( ANIMCHANNEL_LEGS, blendFrames ); + } else if ( channel == ANIMCHANNEL_LEGS ) { + owner->Event_EnableAnim( ANIMCHANNEL_TORSO, blendFrames ); + } + } +} + +bool rvmAIObject::InAnimState( int channel, const char* stateName ) const { + const int index = ChannelIndex( channel ); + if ( index < 0 || !stateName ) { + return false; + } + return !idStr::Icmp( currentAnimState[index].c_str(), stateName ); +} + +void rvmAIObject::StartAction( const char* actionName ) { + currentAction = actionName ? actionName : ""; + ClearParms( actionParms ); +} + +bool rvmAIObject::InAction() const { + return currentAction.Length() != 0; +} + +void rvmAIObject::FinishAction( const char* actionName ) { + if ( !actionName || !waitActionName.Length() || !idStr::Icmp( waitActionName.c_str(), actionName ) ) { + waitActionName.Clear(); + } +} + +bool rvmAIObject::ActionDone( const char* actionName ) const { + if ( !waitActionName.Length() ) { + return true; + } + return actionName && idStr::Icmp( waitActionName.c_str(), actionName ) != 0; +} + +stateResult_t rvmAIObject::RunStateByName( const char* stateName, stateParms_t* parms ) { + return SRESULT_DONE; +} + +int rvmAIObject::ChannelIndex( int channel ) const { + if ( channel == ANIMCHANNEL_HEAD ) { + return 0; + } + if ( channel == ANIMCHANNEL_TORSO ) { + return 1; + } + if ( channel == ANIMCHANNEL_LEGS ) { + return 2; + } + if ( channel == ANIMCHANNEL_ALL ) { + return 3; + } + return -1; +} + +void rvmAIObject::ClearParms( stateParms_t& parms ) { + memset( &parms, 0, sizeof( parms ) ); +} + +float rvmAIObject::Time() const { + return MS2SEC( gameLocal.time ); +} + +float rvmAIObject::FrameTime() const { + return MS2SEC( gameLocal.msec ); +} + +float rvmAIObject::RandomFloat( float max ) const { + return gameLocal.random.RandomFloat() * max; +} + +float rvmAIObject::RandomDelay( float minSeconds, float maxSeconds ) const { + return Time() + minSeconds + RandomFloat( maxSeconds - minSeconds ); +} + +float rvmAIObject::DelayTime( float seconds ) const { + return Time() + seconds; +} + +float rvmAIObject::RandomTime( float seconds ) const { + return Time() + RandomFloat( seconds ); +} + +bool rvmAIObject::InfluenceActive() const { + // Some branches keep the old DoomScript influence query only in script/event glue, + // and idPlayer has no public InfluenceActive() accessor. Keep this wrapper compile-safe; + // wire it to your branch's real influence flag here if one exists. + return false; +} + +idPlayer* rvmAIObject::Player() const { + return gameLocal.GetLocalPlayer(); +} + +void rvmAIObject::IdleAnim( int channel, const char* animName ) { + if ( owner ) { + owner->Event_IdleAnim( channel, animName ); + } +} + +void rvmAIObject::PlayCycle( int channel, const char* animName ) { + if ( owner ) { + owner->Event_PlayCycle( channel, animName, 1.0f); + } +} + +void rvmAIObject::PlayAnim( int channel, const char* animName, bool blend ) { + if ( owner ) { + // idActor::Event_PlayAnim takes channel + anim name; the weapon-native bool + // parameter does not exist on actors. + owner->Event_PlayAnim( channel, animName, blend ); + } +} + +void rvmAIObject::StopAnim( int channel, int frames ) { + if ( owner ) { + owner->Event_StopAnim( channel, frames ); + } +} + +bool rvmAIObject::AnimDone( int channel, int blendFrames ) const { + return owner ? owner->AnimDone( channel, blendFrames ) : true; +} + +bool rvmAIObject::HasAnim( int channel, const char* animName ) const { + return owner ? owner->GetAnim( channel, animName ) != 0 : false; +} + +const char* rvmAIObject::GetPainAnim() const { + if ( !owner ) { + return "pain"; + } + + const char* animName = owner->spawnArgs.GetString( "anim_pain", "" ); + if ( animName && animName[0] ) { + return animName; + } + + animName = owner->spawnArgs.GetString( "pain_anim", "" ); + if ( animName && animName[0] ) { + return animName; + } + + return "pain"; +} + +void rvmAIObject::SetBlendFrames( int channel, int blendFrames ) { + if ( owner ) { + owner->Event_SetBlendFrames( channel, blendFrames ); + } +} + +void rvmAIObject::OverrideAnim( int channel ) { + if ( owner ) { + owner->Event_OverrideAnim( channel ); + } +} + +void rvmAIObject::SetAnimPrefix( const char* prefix ) { + if ( owner ) { + owner->Event_SetAnimPrefix( prefix ); + } +} + +void rvmAIObject::PreventPain( float seconds ) { + if ( owner ) { + owner->Event_PreventPain( seconds ); + } +} + +void rvmAIObject::LookAtEnemy( float seconds ) { + if ( owner ) { + owner->LookAtEnemy( seconds ); + } +} + +void rvmAIObject::LookAtEntity( idEntity* ent, float seconds ) { + if ( owner ) { + owner->LookAtEntity( ent, seconds ); + } +} + +void rvmAIObject::FaceEnemy() { + if ( owner ) { + owner->FaceEnemy(); + } +} + +void rvmAIObject::FaceEntity( idEntity* ent ) { + if ( owner ) { + owner->FaceEntity( ent ); + } +} + +bool rvmAIObject::FacingIdeal() const { + return owner ? owner->FacingIdeal() : true; +} + +void rvmAIObject::StopMove() { + if ( owner ) { + owner->StopMove(); + } +} + +void rvmAIObject::MoveToEnemy() { + if ( owner ) { + owner->MoveToEnemy(); + } +} + +void rvmAIObject::MoveToEnemyHeight() { + if ( owner ) { + owner->MoveToEnemyHeight(); + } +} + +void rvmAIObject::MoveToEntity( idEntity* ent ) { + if ( owner ) { + owner->MoveToEntity( ent ); + } +} + +void rvmAIObject::MoveToPosition( const idVec3& pos ) { + if ( owner ) { + owner->MoveToPosition( pos ); + } +} + +void rvmAIObject::MoveToAttackPosition( idEntity* ent, const char* animName ) { + if ( owner && ent && animName && animName[0] ) { + const int anim = owner->GetAnim( ANIMCHANNEL_LEGS, animName ); + if ( anim ) { + owner->MoveToAttackPosition( ent, anim ); + } + } +} + +void rvmAIObject::MoveOutOfRange( idEntity* ent, float range ) { + if ( owner ) { + owner->MoveOutOfRange( ent, range ); + } +} + +void rvmAIObject::MoveToCover() { + if ( owner ) { + owner->MoveToCover(); + } +} + +void rvmAIObject::Wander() { + if ( owner ) { + owner->Wander(); + } +} + +bool rvmAIObject::MoveDone() const { + return owner ? owner->MoveDone() : true; +} + +int rvmAIObject::MoveStatus() const { + return owner ? owner->MoveStatus() : MOVE_STATUS_DONE; +} + +bool rvmAIObject::DestinationUnreachable() const { + return owner ? owner->AI_DEST_UNREACHABLE != 0 : true; +} + +idActor* rvmAIObject::GetEnemy() const { + return owner ? owner->GetEnemy() : NULL; +} + +void rvmAIObject::SetEnemy( idEntity* ent ) { + if ( owner ) { + owner->SetEnemy( ent ); + } +} + +void rvmAIObject::ClearEnemy() { + if ( owner ) { + owner->ClearEnemy(); + } +} + +bool rvmAIObject::EnemyVisible() const { + return owner ? owner->AI_ENEMY_VISIBLE != 0 : false; +} + +bool rvmAIObject::EnemyInFOV() const { + return owner ? owner->AI_ENEMY_IN_FOV != 0 : false; +} + +bool rvmAIObject::EnemyDead() const { + return owner ? owner->AI_ENEMY_DEAD != 0 : true; +} + +float rvmAIObject::EnemyRange() const { + return owner ? owner->EnemyRange() : idMath::INFINITY; +} + +bool rvmAIObject::CheckForMelee() const { + return owner ? owner->TestMeleeAttack() != 0 : false; +} + +bool rvmAIObject::CanHitEnemy() const { + return owner ? owner->CanHitEnemy() != 0 : false; +} + +bool rvmAIObject::CanHitEnemyFromAnim( const char* animName ) const { + return owner ? owner->CanHitEnemyFromAnim( animName ) != 0 : false; +} + +bool rvmAIObject::EntityInAttackCone( idEntity* ent ) const { + return owner ? owner->EntityInAttackCone( ent ) != 0 : false; +} + +bool rvmAIObject::CanReachEnemy() const { + return owner ? owner->CanReachEnemy() != 0 : false; +} + +bool rvmAIObject::CanReachEntity( idEntity* ent ) const { + return owner ? owner->CanReachEntity( ent ) != 0 : false; +} + +bool rvmAIObject::CanSeeEntity( idEntity* ent ) const { + return owner ? owner->CanSeeEntity( ent ) != 0 : false; +} + +void rvmAIObject::LocateEnemy() { + if ( owner ) { + owner->LocateEnemy(); + } +} + +bool rvmAIObject::AttackMelee( const char* meleeDefName ) { + return owner ? owner->AttackMelee( meleeDefName ) != 0 : false; +} + +idEntity* rvmAIObject::AttackMissile( const char* jointName ) { + return owner ? owner->AttackMissile( jointName ) : NULL; +} + +void rvmAIObject::MuzzleFlash( const char* jointName ) { + if ( owner ) { + owner->MuzzleFlash( jointName ); + } +} + +idEntity* rvmAIObject::GetCombatNode() { + return owner ? owner->GetCombatNode() : NULL; +} + +bool rvmAIObject::Pain() const { + return owner ? owner->AI_PAIN != 0 : false; +} + +bool rvmAIObject::Activated() const { + return owner ? owner->AI_ACTIVATED != 0 : false; +} + +void rvmAIObject::ClearActivated() { + if ( owner ) { + owner->AI_ACTIVATED = false; + } +} + +bool rvmAIObject::MovingForward() const { + return owner ? owner->AI_FORWARD != 0 : false; +} + +bool rvmAIObject::Blocked() const { + return owner ? owner->AI_BLOCKED != 0 : false; +} + +bool rvmAIObject::ObstacleInPath() const { + return owner ? owner->AI_OBSTACLE_IN_PATH != 0 : false; +} + +void rvmAIObject::AllowMovement( bool allow ) { + if ( owner ) { + owner->AllowMovement( allow ? 1.0f : 0.0f ); + } +} + +void rvmAIObject::SetMoveType( int moveType ) { + if ( owner ) { + owner->SetMoveType( moveType ); + } +} + +float rvmAIObject::GetTurnDelta() const { + return owner ? owner->GetTurnDelta() : 0.0f; +} + +void rvmAIObject::TurnTo( float yaw ) { + if ( owner ) { + owner->TurnTo( yaw ); + } +} + +float rvmAIObject::CurrentYaw() const { + return owner ? owner->GetCurrentYaw() : 0.0f; +} + +void rvmAIObject::TurnToPos( const idVec3& pos ) { + if ( owner ) { + owner->TurnToPos( pos ); + } +} + +idVec3 rvmAIObject::GetOrigin() const { + return owner ? owner->GetPhysics()->GetOrigin() : vec3_zero; +} + +idVec3 rvmAIObject::GetEnemyPos() const { + return owner ? owner->GetEnemyPos() : GetOrigin(); +} + +idVec3 rvmAIObject::PredictEnemyPos( float seconds ) const { + return owner ? owner->PredictEnemyPos( seconds ) : GetOrigin(); +} + +idVec3 rvmAIObject::GetLinearVelocity() const { + return owner ? owner->GetPhysics()->GetLinearVelocity() : vec3_zero; +} + +void rvmAIObject::SetLinearVelocity( const idVec3& velocity ) { + if ( owner ) { + owner->GetPhysics()->SetLinearVelocity( velocity ); + } +} + +float rvmAIObject::AnimLength( int channel, const char* animName ) const { + if ( !owner || !animName || !animName[0] ) { + return 0.0f; + } + const int anim = owner->GetAnim( channel, animName ); + if ( !anim || !owner->GetAnimator() ) { + return 0.0f; + } + return MS2SEC( owner->GetAnimator()->AnimLength( anim ) ); +} + +const char* rvmAIObject::ChooseAnim( int channel, const char* animName ) const { + if ( !owner || !animName || !animName[0] ) { + return ""; + } + + const int anim = owner->GetAnim( channel, animName ); + if ( !anim ) { + return ""; + } + + if ( channel == ANIMCHANNEL_HEAD ) { + return animName; + } + + return owner->GetAnimator() ? owner->GetAnimator()->AnimFullName( anim ) : ""; +} + +bool rvmAIObject::TestAnimMove( const char* animName ) const { + return owner ? owner->TestAnimMove( animName ) != 0 : false; +} + +bool rvmAIObject::TestAnimMoveTowardEnemy( const char* animName ) const { + return owner ? owner->TestAnimMoveTowardEnemy( animName ) != 0 : false; +} + +bool rvmAIObject::TestAnimAttack( const char* animName ) const { + return owner ? owner->TestAnimAttack( animName ) != 0 : false; +} + +bool rvmAIObject::TestMoveToPosition( const idVec3& pos ) const { + return owner ? owner->TestMoveToPosition( pos ) != 0 : false; +} + +float rvmAIObject::TravelDistanceToPoint( const idVec3& pos ) const { + return owner ? owner->TravelDistanceToPoint( pos ) : idMath::INFINITY; +} + +float rvmAIObject::TravelDistanceToEntity( idEntity* ent ) const { + return ( owner && ent ) ? owner->TravelDistanceToEntity( ent ) : idMath::INFINITY; +} + +float rvmAIObject::TravelDistanceBetweenPoints( const idVec3& source, const idVec3& dest ) const { + return owner ? owner->TravelDistanceBetweenPoints( source, dest ) : idMath::INFINITY; +} + +idVec3 rvmAIObject::GetReachableEntityPosition( idEntity* ent ) const { + return ( owner && ent ) ? owner->GetReachableEntityPosition( ent ) : vec3_zero; +} + +idVec3 rvmAIObject::GetJumpVelocity( const idVec3& pos, float speed, float maxHeight ) const { + return owner ? owner->GetJumpVelocity( pos, speed, maxHeight ) : vec3_zero; +} + +void rvmAIObject::AttackBegin( const char* damageDefName ) { + if ( owner ) { + owner->BeginAttack( damageDefName ); + } +} + +void rvmAIObject::AttackEnd() { + if ( owner ) { + owner->EndAttack(); + } +} + +void rvmAIObject::ChargeAttack( const char* damageDefName ) { + if ( owner ) { + owner->ChargeAttack( damageDefName ); + } +} + +float rvmAIObject::TestChargeAttack() const { + return owner ? owner->TestChargeAttack() : 0.0f; +} + +idEntity* rvmAIObject::CreateMissile( const char* jointName ) { + return owner ? owner->CreateMissile( jointName ) : NULL; +} + +idEntity* rvmAIObject::FireMissileAtTarget( const char* jointName, const char* targetName ) { + return owner ? owner->FireMissileAtTarget( jointName, targetName ) : NULL; +} + +idEntity* rvmAIObject::LaunchMissile( const idVec3& origin, const idAngles& angles ) { + return owner ? owner->LaunchMissile( origin, angles ) : NULL; +} + +void rvmAIObject::DirectDamage( idEntity* target, const char* damageDefName ) { + if ( owner && target ) { + owner->DirectDamage( target, damageDefName ); + } +} + +void rvmAIObject::RadiusDamageFromJoint( const char* jointName, const char* damageDefName ) { + if ( owner ) { + owner->RadiusDamageFromJoint( jointName, damageDefName ); + } +} + +void rvmAIObject::SetFlySpeed( float speed ) { + if ( owner ) { + owner->SetFlySpeed( speed ); + } +} + +void rvmAIObject::SetFlyOffset( int offset ) { + if ( owner ) { + owner->SetFlyOffset( offset ); + } +} + +void rvmAIObject::ClearFlyOffset() { + if ( owner ) { + owner->ClearFlyOffset(); + } +} + +void rvmAIObject::SetHealth( float value ) { + if ( owner ) { + owner->SetHealth( value ); + } +} + +float rvmAIObject::GetHealth() const { + return owner ? owner->GetHealth() : 0.0f; +} + +void rvmAIObject::AllowDamage() { + if ( owner ) { + owner->AllowDamage(); + } +} + +void rvmAIObject::IgnoreDamage() { + if ( owner ) { + owner->IgnoreDamage(); + } +} + +void rvmAIObject::BecomeSolid() { + if ( owner ) { + owner->BecomeSolid(); + } +} + +void rvmAIObject::BecomeNonSolid() { + if ( owner ) { + owner->BecomeNonSolid(); + } +} + +void rvmAIObject::PreBurn() { + if ( owner ) { + owner->PreBurn(); + } +} + +void rvmAIObject::Burn() { + if ( owner ) { + owner->Burn(); + } +} + +void rvmAIObject::ClearBurn() { + if ( owner ) { + owner->ClearBurn(); + } +} + +void rvmAIObject::KillEntity( idEntity* ent ) { + if ( !ent ) { + return; + } + if ( ent->IsType( idAI::Type ) ) { + static_cast( ent )->Kill(); + } else { + ent->PostEventMS( &EV_Remove, 0 ); + } +} + +idEntity* rvmAIObject::RandomPathFrom( idEntity* from ) const { + return from ? idPathCorner::RandomPath( from, NULL ) : NULL; +} + +idEntity* rvmAIObject::FindEnemy( bool useFOV ) { + return owner ? owner->FindEnemy( useFOV ? 1 : 0 ) : NULL; +} + +idEntity* rvmAIObject::FindEnemyAI( bool useFOV ) { + return owner ? owner->FindEnemyAI( useFOV ? 1 : 0 ) : NULL; +} + +idEntity* rvmAIObject::HeardSound( bool ignoreTeam ) { + return owner ? owner->HeardSound( ignoreTeam ? 1 : 0 ) : NULL; +} + +idEntity* rvmAIObject::FindActorsInBounds( const idVec3& mins, const idVec3& maxs ) { + return owner ? owner->FindActorsInBounds( mins, maxs ) : NULL; +} + +idEntity* rvmAIObject::ClosestReachableEnemyOfEntity( idEntity* teamMate ) { + return ( owner && teamMate ) ? owner->ClosestReachableEnemyOfEntity( teamMate ) : NULL; +} + +idEntity* rvmAIObject::GetRandomTarget( const char* type ) { + return owner ? owner->GetRandomTarget( type ) : NULL; +} + +idVec3 rvmAIObject::GetVectorKey( const char* key, const char* defaultValue ) const { + return owner ? owner->spawnArgs.GetVector( key, defaultValue ) : vec3_zero; +} + +void rvmAIObject::HideEntity( idEntity* ent ) { + if ( ent ) { + ent->Hide(); + } +} + +void rvmAIObject::ShowEntity( idEntity* ent ) { + if ( ent ) { + ent->Show(); + } +} + +void rvmAIObject::RemoveEntity( idEntity* ent ) { + if ( ent ) { + ent->PostEventMS( &EV_Remove, 0 ); + } +} + +int rvmAIObject::GetIntKey( const char* key, const char* defaultValue ) const { + return owner ? owner->spawnArgs.GetInt( key, defaultValue ) : atoi( defaultValue ); +} + +int rvmAIObject::GetIntKey( const char* key, int defaultValue ) const { + if ( !owner ) { + return defaultValue; + } + return owner->spawnArgs.GetInt( key, va( "%d", defaultValue ) ); +} + +float rvmAIObject::GetFloatKey( const char* key, const char* defaultValue ) const { + return owner ? owner->spawnArgs.GetFloat( key, defaultValue ) : atof( defaultValue ); +} + +float rvmAIObject::GetFloatKey( const char* key, float defaultValue ) const { + if ( !owner ) { + return defaultValue; + } + return owner->spawnArgs.GetFloat( key, va( "%g", defaultValue ) ); +} + +const char* rvmAIObject::GetKey( const char* key, const char* defaultValue ) const { + return owner ? owner->spawnArgs.GetString( key, defaultValue ) : defaultValue; +} + +idEntity* rvmAIObject::GetEntityKey( const char* key ) const { + if ( !owner ) { + return NULL; + } + const char* name = owner->spawnArgs.GetString( key, "" ); + return name && name[0] ? gameLocal.FindEntity( name ) : NULL; +} + +void rvmAIObject::HideOwner() { + if ( owner ) { + owner->Hide(); + } +} + +void rvmAIObject::ShowOwner() { + if ( owner ) { + owner->Show(); + } +} + +bool rvmAIObject::CanBecomeSolid() const { + return owner ? owner->CanBecomeSolid() != 0.0f : false; +} + +void rvmAIObject::SetNeverDormant( bool value ) { + if ( owner ) { + owner->fl.neverDormant = value; + } +} + +void rvmAIObject::WakeOnFlashlight( bool value ) { + if ( owner ) { + owner->WakeOnFlashlight( value ? 1 : 0 ); + } +} + +void rvmAIObject::StartSoundKey( const char* key, int channel, bool netSync ) { + if ( owner ) { + owner->StartSound( key, channel, netSync ? SSF_GLOBAL : 0, false, NULL ); + } +} + +void rvmAIObject::StartFx( const char* fx ) { + if ( owner && fx && fx[0] ) { + idEntityFx::StartFx( fx, NULL, NULL, owner, true ); + } +} + +void rvmAIObject::SetSmokeVisibility( int index, bool visible ) { + if ( owner ) { + owner->SetSmokeVisibility( index, visible ? 1 : 0 ); + } +} + +void rvmAIObject::TriggerParticles( const char* jointName ) { + if ( owner ) { + owner->TriggerParticles( jointName ); + } +} + +void rvmAIObject::BecomeRagdoll() { + if ( owner ) { + owner->BecomeRagdoll(); + } +} + +void rvmAIObject::StopThinking() { + if ( owner ) { + owner->StopThinking(); + } +} + +void rvmAIObject::RemoveOwner() { + if ( owner ) { + owner->PostEventMS( &EV_Remove, 0 ); + } +} diff --git a/neo/doom3/game/ai/AI_base.h b/neo/doom3/game/ai/AI_base.h new file mode 100644 index 00000000..6e9c8f21 --- /dev/null +++ b/neo/doom3/game/ai/AI_base.h @@ -0,0 +1,226 @@ +// AI_base.h +// Native state-machine replacement for ai_base.script. +// Root object mirrors DoomScript object "ai" and owns an idAI native actor. + +#pragma once + + +#ifndef ATTACK_DODGE_LEFT +#define ATTACK_DODGE_LEFT 1 +#define ATTACK_DODGE_RIGHT 2 +#define ATTACK_COMBAT_NODE 4 +#define ATTACK_MELEE 8 +#define ATTACK_LEAP 16 +#define ATTACK_MISSILE 32 +#define ATTACK_SPECIAL1 64 +#define ATTACK_SPECIAL2 128 +#define ATTACK_SPECIAL3 256 +#define ATTACK_SPECIAL4 512 +#endif + +#ifndef AI_NOT_ACTIVATED +#define AI_NOT_ACTIVATED 0 +#define AI_CHASING_ENEMY 1 +#define AI_LOST 2 +#define AI_PATH_FOLLOWING 3 +#define AI_ATTACK_NODE 4 +#endif + + +// The weapon native scripts use stateResult_t/stateParms_t. If this port is dropped +// into a branch where those are only declared in the weapon layer, include that header +// before this one or move the enum/struct into a shared script-state header. + +class rvmAIObject : public idClass { +public: + CLASS_PROTOTYPE( rvmAIObject ); + + rvmAIObject(); + virtual ~rvmAIObject(); + + virtual void Init( idAI* aiOwner ); + virtual void Destroy(); + virtual void Update(); + + virtual void SetState( const char* stateName ); + virtual void SetAnimState( int channel, const char* stateName, int blendFrames ); + virtual bool InAnimState( int channel, const char* stateName ) const; + virtual void StartAction( const char* actionName ); + virtual bool InAction() const; + virtual void FinishAction( const char* actionName ); + virtual bool ActionDone( const char* actionName ) const; + + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ); + +protected: + static const int AI_NATIVE_MAX_CHANNELS = 4; + + idAI* owner; + idStr currentState; + idStr currentAction; + idStr currentAnimState[AI_NATIVE_MAX_CHANNELS]; + idStr waitActionName; + + stateParms_t stateParms; + stateParms_t actionParms; + stateParms_t animParms[AI_NATIVE_MAX_CHANNELS]; + + float scratchEndTime; + float scratchMinTime; + float scratchNextTime; + float scratchCount; + float scratchFlags; + bool scratchBool; + idEntityPtr scratchEntity; + idStr scratchString; + + virtual int ChannelIndex( int channel ) const; + virtual void ClearParms( stateParms_t& parms ); + + virtual float Time() const; + virtual float FrameTime() const; + virtual float RandomFloat( float max = 1.0f ) const; + virtual float RandomDelay( float minSeconds, float maxSeconds ) const; + virtual float DelayTime( float seconds ) const; + virtual float RandomTime( float seconds ) const; + + virtual bool InfluenceActive() const; + virtual idPlayer* Player() const; + + virtual void IdleAnim( int channel, const char* animName ); + virtual void PlayCycle( int channel, const char* animName ); + virtual void PlayAnim( int channel, const char* animName, bool blend = false ); + virtual void StopAnim( int channel, int frames ); + virtual bool AnimDone( int channel, int blendFrames ) const; + virtual bool HasAnim( int channel, const char* animName ) const; + virtual const char* GetPainAnim() const; + virtual void SetBlendFrames( int channel, int blendFrames ); + virtual void OverrideAnim( int channel ); + virtual void SetAnimPrefix( const char* prefix ); + virtual void PreventPain( float seconds ); + + virtual void LookAtEnemy( float seconds ); + virtual void LookAtEntity( idEntity* ent, float seconds ); + virtual void FaceEnemy(); + virtual void FaceEntity( idEntity* ent ); + virtual bool FacingIdeal() const; + + virtual void StopMove(); + virtual void MoveToEnemy(); + virtual void MoveToEnemyHeight(); + virtual void MoveToEntity( idEntity* ent ); + virtual void MoveToPosition( const idVec3& pos ); + virtual void MoveToAttackPosition( idEntity* ent, const char* animName ); + virtual void MoveOutOfRange( idEntity* ent, float range ); + virtual void MoveToCover(); + virtual void Wander(); + virtual bool MoveDone() const; + virtual int MoveStatus() const; + virtual bool DestinationUnreachable() const; + + virtual idActor* GetEnemy() const; + virtual void SetEnemy( idEntity* ent ); + virtual void ClearEnemy(); + virtual bool EnemyVisible() const; + virtual bool EnemyInFOV() const; + virtual bool EnemyDead() const; + virtual float EnemyRange() const; + virtual bool CheckForMelee() const; + virtual bool CanHitEnemy() const; + virtual bool CanHitEnemyFromAnim( const char* animName ) const; + virtual bool EntityInAttackCone( idEntity* ent ) const; + virtual bool CanReachEnemy() const; + virtual bool CanReachEntity( idEntity* ent ) const; + virtual bool CanSeeEntity( idEntity* ent ) const; + virtual void LocateEnemy(); + + virtual bool AttackMelee( const char* meleeDefName ); + virtual idEntity* AttackMissile( const char* jointName ); + virtual void MuzzleFlash( const char* jointName ); + virtual idEntity* GetCombatNode(); + + virtual bool Pain() const; + virtual bool Activated() const; + virtual void ClearActivated(); + virtual bool MovingForward() const; + virtual bool Blocked() const; + virtual bool ObstacleInPath() const; + virtual void AllowMovement( bool allow ); + virtual void SetMoveType( int moveType ); + virtual float GetTurnDelta() const; + virtual void TurnTo( float yaw ); + virtual void TurnToPos( const idVec3& pos ); + virtual float CurrentYaw() const; + virtual idVec3 GetOrigin() const; + virtual idVec3 GetEnemyPos() const; + virtual idVec3 PredictEnemyPos( float seconds ) const; + virtual idVec3 GetLinearVelocity() const; + virtual void SetLinearVelocity( const idVec3& velocity ); + virtual float AnimLength( int channel, const char* animName ) const; + virtual const char* ChooseAnim( int channel, const char* animName ) const; + virtual bool TestAnimMove( const char* animName ) const; + virtual bool TestAnimMoveTowardEnemy( const char* animName ) const; + virtual bool TestAnimAttack( const char* animName ) const; + virtual bool TestMoveToPosition( const idVec3& pos ) const; + virtual float TravelDistanceToPoint( const idVec3& pos ) const; + virtual float TravelDistanceToEntity( idEntity* ent ) const; + virtual float TravelDistanceBetweenPoints( const idVec3& source, const idVec3& dest ) const; + virtual idVec3 GetReachableEntityPosition( idEntity* ent ) const; + virtual idVec3 GetJumpVelocity( const idVec3& pos, float speed, float maxHeight ) const; + virtual void AttackBegin( const char* damageDefName ); + virtual void AttackEnd(); + virtual void ChargeAttack( const char* damageDefName ); + virtual float TestChargeAttack() const; + virtual idEntity* CreateMissile( const char* jointName ); + virtual idEntity* FireMissileAtTarget( const char* jointName, const char* targetName ); + virtual idEntity* LaunchMissile( const idVec3& origin, const idAngles& angles ); + virtual void DirectDamage( idEntity* target, const char* damageDefName ); + virtual void RadiusDamageFromJoint( const char* jointName, const char* damageDefName ); + virtual void SetFlySpeed( float speed ); + virtual void SetFlyOffset( int offset ); + virtual void ClearFlyOffset(); + virtual void SetHealth( float value ); + virtual float GetHealth() const; + virtual void AllowDamage(); + virtual void IgnoreDamage(); + virtual void BecomeSolid(); + virtual void BecomeNonSolid(); + virtual void PreBurn(); + virtual void Burn(); + virtual void ClearBurn(); + virtual void KillEntity( idEntity* ent ); + virtual idEntity* RandomPathFrom( idEntity* from ) const; + virtual idEntity* FindEnemy( bool useFOV ); + virtual idEntity* FindEnemyAI( bool useFOV ); + virtual idEntity* HeardSound( bool ignoreTeam ); + virtual idEntity* FindActorsInBounds( const idVec3& mins, const idVec3& maxs ); + virtual idEntity* ClosestReachableEnemyOfEntity( idEntity* teamMate ); + virtual idEntity* GetRandomTarget( const char* type ); + virtual idVec3 GetVectorKey( const char* key, const char* defaultValue = "0 0 0" ) const; + virtual void HideEntity( idEntity* ent ); + virtual void ShowEntity( idEntity* ent ); + virtual void RemoveEntity( idEntity* ent ); + + virtual int GetIntKey( const char* key, const char* defaultValue = "0" ) const; + virtual int GetIntKey( const char* key, int defaultValue ) const; + virtual float GetFloatKey( const char* key, const char* defaultValue = "0" ) const; + virtual float GetFloatKey( const char* key, float defaultValue ) const; + virtual const char* GetKey( const char* key, const char* defaultValue = "" ) const; + virtual idEntity* GetEntityKey( const char* key ) const; + + virtual void HideOwner(); + virtual void ShowOwner(); + virtual bool CanBecomeSolid() const; + virtual void SetNeverDormant( bool value ); + virtual void WakeOnFlashlight( bool value ); + virtual void StartSoundKey( const char* key, int channel, bool netSync = false ); + virtual void StartFx( const char* fx ); + virtual void SetSmokeVisibility( int index, bool visible ); + virtual void TriggerParticles( const char* jointName ); + virtual void BecomeRagdoll(); + virtual void StopThinking(); + virtual void RemoveOwner(); +}; + +#define AI_DISPATCH_STATE( stateNameLiteral, methodName ) \ + if ( !idStr::Icmp( stateName, stateNameLiteral ) ) { return methodName( parms ); } diff --git a/neo/doom3/game/ai/AI_character.cpp b/neo/doom3/game/ai/AI_character.cpp new file mode 100644 index 00000000..bbe13f12 --- /dev/null +++ b/neo/doom3/game/ai/AI_character.cpp @@ -0,0 +1,1403 @@ +// AI_character.cpp +// Native state-machine replacement for ai_character.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +namespace { + const float CHAR_WALKTURN = 65.0f; + const float CHAR_MIN_TURN = 10.0f; + const float CHAR_TURN_THRESHOLD = 45.0f; + const float CHAR_BLEND_COWER_TO_IDLE = 6.0f; + const float CHAR_BLEND_TURN_TO_IDLE = 0.0f; +} + +CLASS_DECLARATION( rvmAIObject, rvmCharacter ) +END_CLASS + +rvmCharacter::rvmCharacter() { + talkMode = CHAR_TALK_PRIMARY; + talkSecondaryIndex = 1; + customBlendOut = 0.0f; + allowTurn = false; + run = false; + canTalk = false; + noCower = false; + noCowerSaved = false; + ignorePush = false; + skipOutOfPath = false; + followNodes = true; + dontPushOthers = false; + lastPushTime = 0.0f; + pushTime = 0.0f; + talkEndTime = 0.0f; + talkChannel = ANIMCHANNEL_LEGS; + talkOriginalYaw = 0.0f; +} + +void rvmCharacter::Init( idAI* aiOwner ) { + rvmAIObject::Init( aiOwner ); + + if ( owner ) { + owner->SetJointMod( GetIntKey( "head_look" ) ); + owner->spawnArgs.Set( "conversationNext", "" ); + } + + lastPushTime = 0.0f; + pushTime = 0.0f; + talkMode = CHAR_TALK_PRIMARY; + talkSecondaryIndex = 1; + followNodes = true; + walkAnim.Clear(); + run = false; + canTalk = GetIntKey( "talks" ) != 0; + noCower = GetIntKey( "no_cower" ) != 0; + ignorePush = GetIntKey( "ignore_push" ) != 0; + noCowerSaved = noCower; + dontPushOthers = GetIntKey( "dont_push_others" ) != 0; + allowTurn = GetFloatKey( "turn_rate" ) != 0.0f; + if ( !allowTurn ) { + ignorePush = true; + } + + SetState( GetIntKey( "spawner" ) ? "state_Spawner" : "state_Begin" ); +} + +void rvmCharacter::Destroy() { + if ( listening_to_character.GetEntity() && listening_to_character.GetEntity()->IsType( idAI::Type ) ) { + idAI* listeningAI = static_cast( listening_to_character.GetEntity() ); + rvmCharacter* conversation = listeningAI->nativeAIObject ? listeningAI->nativeAIObject->Cast() : NULL; + while ( conversation ) { + if ( conversation->next_listener.GetEntity() == owner ) { + conversation->next_listener = next_listener; + break; + } + idAI* nextAI = conversation->next_listener.GetEntity() && conversation->next_listener.GetEntity()->IsType( idAI::Type ) + ? static_cast( conversation->next_listener.GetEntity() ) + : NULL; + conversation = nextAI && nextAI->nativeAIObject ? nextAI->nativeAIObject->Cast() : NULL; + } + } + + rvmAIObject::Destroy(); +} + +stateResult_t rvmCharacter::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Anim_Disable", Anim_Disable ); + AI_DISPATCH_STATE( "Head_Idle", Head_Idle ); + AI_DISPATCH_STATE( "Head_Dead", Head_Dead ); + AI_DISPATCH_STATE( "Head_TalkHeadOnly", Head_TalkHeadOnly ); + AI_DISPATCH_STATE( "Legs_Death", Legs_Death ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Walk_Special", Legs_Walk_Special ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_TurnLeft", Legs_TurnLeft ); + AI_DISPATCH_STATE( "Legs_TurnRight", Legs_TurnRight ); + AI_DISPATCH_STATE( "Legs_Cower", Legs_Cower ); + AI_DISPATCH_STATE( "Legs_CustomCycle", Legs_CustomCycle ); + AI_DISPATCH_STATE( "Legs_CustomAnim", Legs_CustomAnim ); + AI_DISPATCH_STATE( "state_Spawner", state_Spawner ); + AI_DISPATCH_STATE( "state_Cower", state_Cower ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_TalkTrigger", state_TalkTrigger ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "idle_stand", idle_stand ); + AI_DISPATCH_STATE( "idle_followPathEntities", idle_followPathEntities ); + AI_DISPATCH_STATE( "idle_talk", idle_talk ); + AI_DISPATCH_STATE( "checkBlocked", checkBlocked ); + AI_DISPATCH_STATE( "path_corner", path_corner ); + AI_DISPATCH_STATE( "path_anim", path_anim ); + AI_DISPATCH_STATE( "path_cycleanim", path_cycleanim ); + AI_DISPATCH_STATE( "path_turn", path_turn ); + AI_DISPATCH_STATE( "path_wait", path_wait ); + AI_DISPATCH_STATE( "path_waitfortrigger", path_waitfortrigger ); + AI_DISPATCH_STATE( "path_hide", path_hide ); + AI_DISPATCH_STATE( "path_show", path_show ); + AI_DISPATCH_STATE( "path_conversation_listen", path_conversation_listen ); + AI_DISPATCH_STATE( "path_conversation", path_conversation ); + AI_DISPATCH_STATE( "path_headanim", path_headanim ); + AI_DISPATCH_STATE( "path_waitforheadanim", path_waitforheadanim ); + AI_DISPATCH_STATE( "path_talk", path_talk ); + AI_DISPATCH_STATE( "path_talk_triggered", path_talk_triggered ); + AI_DISPATCH_STATE( "path_talk_primary", path_talk_primary ); + AI_DISPATCH_STATE( "path_talk_secondary", path_talk_secondary ); + AI_DISPATCH_STATE( "path_lookat", path_lookat ); + AI_DISPATCH_STATE( "state_FollowAlternatePath", state_FollowAlternatePath ); + return rvmAIObject::RunStateByName( stateName, parms ); +} + +stateResult_t rvmCharacter::Anim_Disable( stateParms_t* parms ) { + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Head_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_HEAD, "stand" ); + parms->stage = 1; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Head_Dead( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayAnim( ANIMCHANNEL_HEAD, "dead" ); + parms->stage = 1; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Head_TalkHeadOnly( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayAnim( ANIMCHANNEL_HEAD, customAnim.c_str() ); + parms->stage = 1; + } + if ( AnimDone( ANIMCHANNEL_HEAD, idMath::Ftoi( customBlendOut ) ) ) { + FinishAction( "talkAnim" ); + SetAnimState( ANIMCHANNEL_HEAD, "Head_Idle", idMath::Ftoi( customBlendOut ) ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Legs_Death( stateParms_t* parms ) { + FinishAction( "dead" ); + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + AllowMovement( false ); + IdleAnim( ANIMCHANNEL_LEGS, "stand" ); + parms->stage = 1; + } + + if ( MovingForward() ) { + if ( walkAnim.Length() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk_Special", 8 ); + } else if ( run ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); + } else { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); + } + return SRESULT_DONE; + } + + if ( allowTurn ) { + const float delta = GetTurnDelta(); + if ( delta > CHAR_MIN_TURN ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_TurnLeft", 4 ); + return SRESULT_DONE; + } + if ( delta < -CHAR_MIN_TURN ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_TurnRight", 4 ); + return SRESULT_DONE; + } + } + + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + AllowMovement( true ); + PlayCycle( ANIMCHANNEL_LEGS, "walk" ); + parms->stage = 1; + } + if ( !MovingForward() || walkAnim.Length() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + return SRESULT_DONE; + } + if ( run ) { + const float delta = GetTurnDelta(); + if ( delta < CHAR_WALKTURN && delta > -CHAR_WALKTURN ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); + return SRESULT_DONE; + } + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Legs_Walk_Special( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + AllowMovement( true ); + PlayCycle( ANIMCHANNEL_LEGS, walkAnim.c_str() ); + parms->stage = 1; + } + if ( !MovingForward() || !walkAnim.Length() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + AllowMovement( true ); + PlayCycle( ANIMCHANNEL_LEGS, "run" ); + parms->stage = 1; + } + const float delta = GetTurnDelta(); + if ( !MovingForward() || !run || walkAnim.Length() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + return SRESULT_DONE; + } + if ( delta > CHAR_WALKTURN || delta < -CHAR_WALKTURN ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Legs_TurnLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + AllowMovement( true ); + if ( owner ) { + owner->AnimTurn( 180.0f ); + } + PlayAnim( ANIMCHANNEL_LEGS, "turn_left" ); + parms->stage = 1; + } + if ( !AnimDone( ANIMCHANNEL_LEGS, idMath::Ftoi( CHAR_BLEND_TURN_TO_IDLE ) ) ) { + return SRESULT_WAIT; + } + if ( owner ) { + owner->AnimTurn( 0.0f ); + } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( CHAR_BLEND_TURN_TO_IDLE ) ); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::Legs_TurnRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + AllowMovement( true ); + if ( owner ) { + owner->AnimTurn( 180.0f ); + } + PlayAnim( ANIMCHANNEL_LEGS, "turn_right" ); + parms->stage = 1; + } + if ( !AnimDone( ANIMCHANNEL_LEGS, idMath::Ftoi( CHAR_BLEND_TURN_TO_IDLE ) ) ) { + return SRESULT_WAIT; + } + if ( owner ) { + owner->AnimTurn( 0.0f ); + } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( CHAR_BLEND_TURN_TO_IDLE ) ); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::Legs_Cower( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + if ( owner ) { + owner->SetTalkTarget( NULL ); + } + StartSoundKey( "snd_cower", SND_CHANNEL_VOICE, false ); + PlayAnim( ANIMCHANNEL_LEGS, "cower" ); + parms->stage = 1; + } + LookAtEntity( Player(), 0.1f ); + if ( AnimDone( ANIMCHANNEL_LEGS, idMath::Ftoi( CHAR_BLEND_COWER_TO_IDLE ) ) ) { + if ( owner ) { + owner->SetTalkTarget( NULL ); + } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( CHAR_BLEND_COWER_TO_IDLE ) ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Legs_CustomCycle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + AllowMovement( true ); + PlayCycle( ANIMCHANNEL_LEGS, customAnim.c_str() ); + parms->stage = 1; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::Legs_CustomAnim( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + AllowMovement( true ); + PlayAnim( ANIMCHANNEL_LEGS, customAnim.c_str() ); + parms->stage = 1; + } + if ( AnimDone( ANIMCHANNEL_LEGS, idMath::Ftoi( customBlendOut ) ) ) { + FinishAction( "customAnim" ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( customBlendOut ) ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::state_Spawner( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + HideOwner(); + ClearActivated(); + parms->stage = 1; + } + if ( Activated() && CanBecomeSolid() ) { + ClearActivated(); + if ( owner ) { + idDict args; + args = owner->spawnArgs; + args.SetInt( "spawner", 0 ); + gameLocal.SpawnEntityDef( args ); + } + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::state_Cower( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + if ( owner ) { + owner->StopSound( SND_CHANNEL_VOICE, false ); + owner->SetTalkTarget( NULL ); + } + endHeadAnim( 6.0f ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Cower", 6 ); + parms->stage = 1; + } + if ( InAnimState( ANIMCHANNEL_LEGS, "Legs_Cower" ) ) { + return SRESULT_WAIT; + } + if ( owner ) { + owner->SetTalkTarget( NULL ); + } + SetState( "state_Idle" ); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::state_Begin( stateParms_t* parms ) { + enum { + STAGE_INIT, + STAGE_WAIT_TRIGGER, + STAGE_WAIT_SOLID, + STAGE_TRIGGER_ANIM, + STAGE_TELEPORT_FX, + STAGE_TELEPORT_ANIM, + STAGE_POST_WAIT, + STAGE_WAIT_EXPLICIT_TRIGGER + }; + + switch ( parms->stage ) { + case STAGE_INIT: { + talkMode = CHAR_TALK_PRIMARY; + followNodes = true; + walkAnim.Clear(); + run = false; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + SetAnimState( ANIMCHANNEL_HEAD, "Head_Idle", 0 ); + + const char* triggerAnim = GetKey( "trigger_anim" ); + if ( triggerAnim[0] ) { + customAnim = triggerAnim; + customBlendOut = 4.0f; + HideOwner(); + ClearActivated(); + parms->stage = STAGE_WAIT_TRIGGER; + return SRESULT_WAIT; + } + + const int teleportType = GetIntKey( "teleport" ); + if ( teleportType > 0 ) { + HideOwner(); + ClearActivated(); + parms->stage = STAGE_WAIT_TRIGGER; + return SRESULT_WAIT; + } + + if ( GetIntKey( "hide" ) ) { + HideOwner(); + ClearActivated(); + parms->stage = STAGE_WAIT_TRIGGER; + return SRESULT_WAIT; + } + + if ( GetIntKey( "trigger" ) ) { + ClearActivated(); + parms->stage = STAGE_WAIT_EXPLICIT_TRIGGER; + return SRESULT_WAIT; + } + + scratchEndTime = GetFloatKey( "wait" ) > 0.0f ? DelayTime( GetFloatKey( "wait" ) ) : 0.0f; + parms->stage = STAGE_POST_WAIT; + return SRESULT_WAIT; + } + case STAGE_WAIT_TRIGGER: + if ( !Activated() ) { + return SRESULT_WAIT; + } + ClearActivated(); + ClearEnemy(); + parms->stage = STAGE_WAIT_SOLID; + return SRESULT_WAIT; + case STAGE_WAIT_SOLID: + if ( !CanBecomeSolid() ) { + return SRESULT_WAIT; + } + if ( GetIntKey( "teleport" ) > 0 ) { + BecomeSolid(); + SetMoveType( MOVETYPE_STATIC ); + const int teleportType = GetIntKey( "teleport" ); + StartFx( teleportType == 1 ? "fx/teleporter1.fx" : + teleportType == 2 ? "fx/teleporter2.fx" : + teleportType == 3 ? "fx/teleporter3.fx" : "fx/teleporter.fx" ); + scratchEndTime = DelayTime( teleportType == 1 ? 1.6f : teleportType == 2 ? 2.6f : teleportType == 3 ? 3.6f : 0.6f ); + parms->stage = STAGE_TELEPORT_FX; + return SRESULT_WAIT; + } + ShowOwner(); + if ( customAnim.Length() ) { + waitForCustomAnim( customAnim.c_str(), 0.0f, 4.0f ); + parms->stage = STAGE_TRIGGER_ANIM; + return SRESULT_WAIT; + } + scratchEndTime = GetFloatKey( "wait" ) > 0.0f ? DelayTime( GetFloatKey( "wait" ) ) : 0.0f; + parms->stage = STAGE_POST_WAIT; + return SRESULT_WAIT; + case STAGE_TRIGGER_ANIM: + if ( InAnimState( ANIMCHANNEL_LEGS, "Legs_CustomAnim" ) ) { + return SRESULT_WAIT; + } + scratchEndTime = GetFloatKey( "wait" ) > 0.0f ? DelayTime( GetFloatKey( "wait" ) ) : 0.0f; + parms->stage = STAGE_POST_WAIT; + return SRESULT_WAIT; + case STAGE_TELEPORT_FX: + if ( Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + ShowOwner(); + playCustomAnim( "teleport", 0.0f, 4.0f ); + parms->stage = STAGE_TELEPORT_ANIM; + return SRESULT_WAIT; + case STAGE_TELEPORT_ANIM: + if ( InAnimState( ANIMCHANNEL_LEGS, "Legs_CustomAnim" ) ) { + return SRESULT_WAIT; + } + SetMoveType( MOVETYPE_ANIM ); + scratchEndTime = GetFloatKey( "wait" ) > 0.0f ? DelayTime( GetFloatKey( "wait" ) ) : 0.0f; + parms->stage = STAGE_POST_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT_EXPLICIT_TRIGGER: + if ( !Activated() ) { + return SRESULT_WAIT; + } + ClearActivated(); + scratchEndTime = GetFloatKey( "wait" ) > 0.0f ? DelayTime( GetFloatKey( "wait" ) ) : 0.0f; + parms->stage = STAGE_POST_WAIT; + return SRESULT_WAIT; + case STAGE_POST_WAIT: + if ( scratchEndTime > 0.0f && Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + SetState( GetIntKey( "talktrigger" ) ? "state_TalkTrigger" : "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharacter::state_TalkTrigger( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + const char* animname = GetKey( "anim" ); + if ( animname[0] ) { + playCustomCycle( animname, 0.0f ); + } + ClearActivated(); + parms->stage = 1; + } + check_cower(); + if ( !Activated() ) { + return SRESULT_WAIT; + } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 12 ); + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + talkMode = CHAR_TALK_TRIGGERED; + SetState( "state_Idle" ); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::state_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + if ( canTalk && owner ) { + owner->SetTalkState( TALK_OK ); + } + noCower = noCowerSaved; + parms->stage = 1; + } + + if ( owner && owner->AI_TALK ) { + StartAction( "idle_talk" ); + return SRESULT_WAIT; + } + + if ( followNodes ) { + if ( !current_path.GetEntity() ) { + current_path = owner ? owner->RandomPath() : NULL; + } + if ( current_path.GetEntity() ) { + executePathCommand( current_path.GetEntity() ); + if ( GetIntKey( "follow_once" ) ) { + followNodes = false; + } + return SRESULT_WAIT; + } + } + + check_cower(); + if ( owner && owner->AI_PUSHED ) { + getOutOfWay(); + owner->AI_PUSHED = false; + } + StopMove(); + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::state_Killed( stateParms_t* parms ) { + StopMove(); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 0 ); + SetAnimState( ANIMCHANNEL_HEAD, "Head_Dead", 0 ); + StopThinking(); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::idle_stand( stateParms_t* parms ) { + StopMove(); + check_cower(); + if ( owner && owner->AI_PUSHED ) { + getOutOfWay(); + owner->AI_PUSHED = false; + } + return ( owner && owner->AI_TALK ) ? SRESULT_DONE : SRESULT_WAIT; +} + +stateResult_t rvmCharacter::idle_followPathEntities( stateParms_t* parms ) { + if ( !current_path.GetEntity() ) { + current_path = owner ? owner->RandomPath() : NULL; + } + if ( current_path.GetEntity() ) { + executePathCommand( current_path.GetEntity() ); + } + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::idle_talk( stateParms_t* parms ) { + enum { STAGE_START, STAGE_TURN_WAIT, STAGE_PLAY, STAGE_TALKING, STAGE_AFTER_TALK }; + idEntity* talkTo = owner ? owner->GetTalkTarget() : NULL; + if ( !talkTo ) { + if ( owner ) { + owner->SetTalkTarget( NULL ); + } + return SRESULT_DONE; + } + + const bool noTurn = GetIntKey( "talk_no_turn" ) != 0; + const float talkRadius = GetFloatKey( "talkradius", 300.0f ); + + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + talkOriginalYaw = owner ? owner->GetAngles().y : 0.0f; + if ( !noTurn && allowTurn ) { + faceTowardsEntity( talkTo, 0.1f ); + scratchEndTime = DelayTime( 0.5f ); + parms->stage = STAGE_TURN_WAIT; + return SRESULT_WAIT; + } + parms->stage = STAGE_PLAY; + return SRESULT_WAIT; + case STAGE_TURN_WAIT: + LookAtEntity( talkTo, 0.1f ); + if ( Time() < scratchEndTime || !InAnimState( ANIMCHANNEL_LEGS, "Legs_Idle" ) ) { + if ( talkMode != CHAR_TALK_TRIGGERED ) { + check_cower(); + } + return SRESULT_WAIT; + } + parms->stage = STAGE_PLAY; + return SRESULT_WAIT; + case STAGE_PLAY: { + AllowMovement( true ); + int blendIn = 8; + if ( talkMode == CHAR_TALK_TRIGGERED ) { + blendIn = GetIntKey( "talk0_blendin", 8 ); + customBlendOut = GetIntKey( "talk0_blendout", 8 ); + talkChannel = playHeadAnim( "talk_trigger", blendIn ); + talkMode = CHAR_TALK_PRIMARY; + } else if ( talkMode == CHAR_TALK_PRIMARY ) { + blendIn = GetIntKey( "talk1_blendin", 8 ); + customBlendOut = GetIntKey( "talk1_blendout", 8 ); + talkChannel = playHeadAnim( "talk_primary", blendIn ); + talkMode = CHAR_TALK_SECONDARY; + } else { + idStr anim = va( "talk_secondary%d", talkSecondaryIndex ); + blendIn = GetIntKey( "talk2_blendin", 8 ); + customBlendOut = GetIntKey( "talk2_blendout", 8 ); + talkChannel = playHeadAnim( anim.c_str(), blendIn ); + talkSecondaryIndex++; + anim = va( "talk_secondary%d", talkSecondaryIndex ); + if ( !HasAnim( ANIMCHANNEL_HEAD, anim.c_str() ) && !HasAnim( ANIMCHANNEL_LEGS, anim.c_str() ) ) { + talkSecondaryIndex = 1; + } + } + parms->stage = STAGE_TALKING; + return SRESULT_WAIT; + } + case STAGE_TALKING: + if ( owner && ( owner->GetPhysics()->GetOrigin() - talkTo->GetPhysics()->GetOrigin() ).Length() > talkRadius ) { + owner->StopSound( SND_CHANNEL_VOICE, false ); + parms->stage = STAGE_AFTER_TALK; + return SRESULT_WAIT; + } + if ( !AnimDone( idMath::Ftoi( talkChannel ), idMath::Ftoi( customBlendOut ) ) ) { + check_cower(); + if ( noTurn || !allowTurn ) { + LookAtEntity( talkTo, 0.1f ); + } else { + faceTowardsEntity( talkTo, 0.1f ); + } + return SRESULT_WAIT; + } + parms->stage = STAGE_AFTER_TALK; + talkEndTime = DelayTime( GetFloatKey( "talktime", 10.0f ) ); + endHeadAnim( customBlendOut ); + if ( owner ) { + owner->SetTalkTarget( NULL ); + } + return SRESULT_WAIT; + case STAGE_AFTER_TALK: + if ( Time() < talkEndTime && owner && ( owner->GetPhysics()->GetOrigin() - talkTo->GetPhysics()->GetOrigin() ).Length() <= talkRadius ) { + check_cower(); + if ( noTurn || !allowTurn ) { + LookAtEntity( talkTo, 0.1f ); + } else { + faceTowardsEntity( talkTo, 0.1f ); + } + if ( owner->AI_TALK ) { + AllowMovement( false ); + return SRESULT_DONE; + } + if ( owner->AI_PUSHED ) { + getOutOfWay(); + owner->AI_PUSHED = false; + } + return SRESULT_WAIT; + } + LookAtEntity( NULL, 0.0f ); + TurnTo( talkOriginalYaw ); + AllowMovement( false ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharacter::checkBlocked( stateParms_t* parms ) { + if ( owner && MoveStatus() >= MOVE_STATUS_BLOCKED_BY_OBJECT ) { + owner->KickObstacles( owner->GetObstacle(), GetFloatKey( "kick_force", 60.0f ) ); + } + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::path_corner( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + if ( parms->stage == 0 ) { + run = node->spawnArgs.GetBool( "run", "0" ); + walkAnim = node->spawnArgs.GetString( "anim", "" ); + MoveToEntity( node ); + parms->stage = 1; + } + check_cower(); + checkBlocked( parms ); + if ( owner && owner->AI_TALK ) { + cancelPathCommand(); + return SRESULT_DONE; + } + if ( MoveDone() || DestinationUnreachable() ) { + walkAnim.Clear(); + run = false; + if ( DestinationUnreachable() && owner ) { + gameLocal.Warning( "entity '%s' couldn't reach path_corner '%s'", owner->GetName(), node->GetName() ); + } + finishPathCommand(); + return SRESULT_DONE; + } + MoveToEntity( node ); + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::path_anim( stateParms_t* parms ) { + enum { STAGE_START, STAGE_TURN, STAGE_ANIM }; + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + switch ( parms->stage ) { + case STAGE_START: + customAnim = node->spawnArgs.GetString( "anim", "" ); + customBlendOut = node->spawnArgs.GetInt( "blend_out", "0" ); + if ( node->spawnArgs.GetString( "angle", "" )[0] ) { + TurnTo( node->spawnArgs.GetFloat( "angle" ) ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + } + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_TURN: + check_cower(); + if ( !FacingIdeal() ) { + return SRESULT_WAIT; + } + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( node->spawnArgs.GetBool( "head_anim", "0" ) ) { + talkChannel = playHeadAnim( customAnim.c_str(), node->spawnArgs.GetInt( "blend_in", "0" ) ); + parms->stage = 3; + return SRESULT_WAIT; + } + playCustomAnim( customAnim.c_str(), node->spawnArgs.GetInt( "blend_in", "0" ), customBlendOut ); + parms->stage = 4; + return SRESULT_WAIT; + case 3: + check_cower(); + if ( owner && owner->AI_TALK ) { + endHeadAnim( customBlendOut ); + cancelPathCommand(); + return SRESULT_DONE; + } + if ( AnimDone( idMath::Ftoi( talkChannel ), idMath::Ftoi( customBlendOut ) ) ) { + endHeadAnim( customBlendOut ); + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_WAIT; + case 4: + check_cower(); + if ( owner && owner->AI_TALK ) { + endCustomAnim(); + cancelPathCommand(); + return SRESULT_DONE; + } + if ( inCustomAnim() ) { + return SRESULT_WAIT; + } + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharacter::path_cycleanim( stateParms_t* parms ) { + enum { STAGE_START, STAGE_TURN, STAGE_HOLD }; + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + switch ( parms->stage ) { + case STAGE_START: + customAnim = node->spawnArgs.GetString( "anim", "" ); + customBlendOut = node->spawnArgs.GetInt( "blend_out", "0" ); + if ( node->spawnArgs.GetString( "angle", "" )[0] ) { + TurnTo( node->spawnArgs.GetFloat( "angle" ) ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + } + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + case STAGE_TURN: + if ( !FacingIdeal() ) { + check_cower(); + return SRESULT_WAIT; + } + playCustomCycle( customAnim.c_str(), node->spawnArgs.GetInt( "blend_in", "0" ) ); + scratchEndTime = node->spawnArgs.GetFloat( "wait", "0" ) > 0.0f ? DelayTime( node->spawnArgs.GetFloat( "wait" ) ) : 0.0f; + ClearActivated(); + parms->stage = STAGE_HOLD; + return SRESULT_WAIT; + case STAGE_HOLD: + check_cower(); + if ( owner && owner->AI_TALK ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( customBlendOut ) ); + cancelPathCommand(); + return SRESULT_DONE; + } + if ( scratchEndTime > 0.0f ) { + if ( Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + } else if ( !Activated() ) { + return SRESULT_WAIT; + } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( customBlendOut ) ); + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharacter::path_turn( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + if ( parms->stage == 0 ) { + TurnTo( node->GetPhysics()->GetAxis()[0].ToYaw() ); + parms->stage = 1; + } + check_cower(); + if ( FacingIdeal() ) { + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::path_wait( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + if ( parms->stage == 0 ) { + scratchEndTime = DelayTime( node->spawnArgs.GetFloat( "wait", "0" ) ); + parms->stage = 1; + } + check_cower(); + if ( Time() >= scratchEndTime ) { + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::path_waitfortrigger( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + ClearActivated(); + parms->stage = 1; + } + check_cower(); + if ( owner && owner->AI_TALK ) { + cancelPathCommand(); + return SRESULT_DONE; + } + if ( Activated() ) { + ClearActivated(); + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::path_hide( stateParms_t* parms ) { + HideOwner(); + finishPathCommand(); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::path_show( stateParms_t* parms ) { + if ( !CanBecomeSolid() ) { + return SRESULT_WAIT; + } + ShowOwner(); + finishPathCommand(); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::path_conversation_listen( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + if ( parms->stage == 0 ) { + StopMove(); + listening_to_character = gameLocal.FindEntity( node->spawnArgs.GetString( "focus", "" ) ); + customAnim = node->spawnArgs.GetString( "anim", "" ); + customBlendOut = node->spawnArgs.GetInt( "blend_out", "0" ); + if ( customAnim.Length() ) { + playCustomCycle( customAnim.c_str(), node->spawnArgs.GetInt( "blend_in", "0" ) ); + } + parms->stage = 1; + } + if ( owner && owner->AI_TALK ) { + cancelPathCommand(); + return SRESULT_DONE; + } + if ( !listening_to_character.GetEntity() ) { + if ( customAnim.Length() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( customBlendOut ) ); + } + finishPathCommand(); + return SRESULT_DONE; + } + if ( !node->spawnArgs.GetBool( "no_look", "0" ) ) { + LookAtEntity( listening_to_character.GetEntity(), 0.1f ); + } + check_cower(); + return SRESULT_WAIT; +} + +stateResult_t rvmCharacter::path_conversation( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM, STAGE_WAIT }; + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + idEntity* listener = gameLocal.FindEntity( node->spawnArgs.GetString( "focus", "" ) ); + switch ( parms->stage ) { + case STAGE_START: { + StopMove(); + const char* dialog = node->spawnArgs.GetString( "snd_dialog", "" ); + if ( dialog[0] && owner ) { + const idSoundShader* shader = declManager->FindSound( dialog ); + owner->StartSoundShader( shader, SND_CHANNEL_VOICE, 0, false, NULL ); + } + customBlendOut = node->spawnArgs.GetInt( "blend_out", "0" ); + talkChannel = playHeadAnim( node->spawnArgs.GetString( "anim", "" ), node->spawnArgs.GetInt( "blend_in", "0" ) ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + } + case STAGE_ANIM: + if ( owner && owner->AI_TALK ) { + break_out_of_conversation(); + cancelPathCommand(); + return SRESULT_DONE; + } + check_cower(); + LookAtEntity( listener, 0.1f ); + if ( !AnimDone( idMath::Ftoi( talkChannel ), idMath::Ftoi( customBlendOut ) ) ) { + return SRESULT_WAIT; + } + endHeadAnim( customBlendOut ); + scratchEndTime = node->spawnArgs.GetFloat( "wait", "0" ) > 0.0f ? DelayTime( node->spawnArgs.GetFloat( "wait" ) ) : 0.0f; + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( owner && owner->AI_TALK ) { + break_out_of_conversation(); + cancelPathCommand(); + return SRESULT_DONE; + } + check_cower(); + LookAtEntity( listener, 0.1f ); + if ( scratchEndTime > 0.0f && Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + while ( next_listener.GetEntity() ) { + idAI* listenerAI = next_listener.GetEntity()->IsType( idAI::Type ) ? static_cast( next_listener.GetEntity() ) : NULL; + rvmCharacter* charListener = listenerAI && listenerAI->nativeAIObject ? listenerAI->nativeAIObject->Cast() : NULL; + if ( charListener ) { + next_listener = charListener->next_listener; + charListener->next_listener = NULL; + charListener->listening_to_character = NULL; + } else { + next_listener = NULL; + } + } + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharacter::path_headanim( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + customBlendOut = node->spawnArgs.GetInt( "blend_out", "0" ); + customAnim = node->spawnArgs.GetString( "anim", "" ); + SetAnimState( ANIMCHANNEL_HEAD, "Head_TalkHeadOnly", node->spawnArgs.GetInt( "blend_in", "0" ) ); + finishPathCommand(); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::path_waitforheadanim( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + idEntity* focus = node ? gameLocal.FindEntity( node->spawnArgs.GetString( "focus", "" ) ) : NULL; + StopMove(); + if ( owner && owner->AI_TALK ) { + cancelPathCommand(); + return SRESULT_DONE; + } + check_cower(); + LookAtEntity( focus, 0.1f ); + if ( InAnimState( ANIMCHANNEL_HEAD, "Head_TalkHeadOnly" ) ) { + return SRESULT_WAIT; + } + finishPathCommand(); + return SRESULT_DONE; +} + +stateResult_t rvmCharacter::path_talk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + parms->stage = 1; + } + stateResult_t result = idle_talk( parms ); + if ( result == SRESULT_DONE ) { + finishPathCommand(); + } + return result; +} + +stateResult_t rvmCharacter::path_talk_triggered( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + talkMode = CHAR_TALK_TRIGGERED; + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + parms->stage = 1; + } + stateResult_t result = idle_talk( parms ); + if ( result == SRESULT_DONE ) { + finishPathCommand(); + } + return result; +} + +stateResult_t rvmCharacter::path_talk_primary( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + talkMode = CHAR_TALK_PRIMARY; + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + parms->stage = 1; + } + stateResult_t result = idle_talk( parms ); + if ( result == SRESULT_DONE ) { + finishPathCommand(); + } + return result; +} + +stateResult_t rvmCharacter::path_talk_secondary( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + talkMode = CHAR_TALK_SECONDARY; + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + parms->stage = 1; + } + stateResult_t result = idle_talk( parms ); + if ( result == SRESULT_DONE ) { + finishPathCommand(); + } + return result; +} + +stateResult_t rvmCharacter::path_lookat( stateParms_t* parms ) { + enum { STAGE_START, STAGE_HOLD }; + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + idEntity* focus = gameLocal.FindEntity( node->spawnArgs.GetString( "focus", "" ) ); + switch ( parms->stage ) { + case STAGE_START: + customAnim = node->spawnArgs.GetString( "anim", "" ); + customBlendOut = node->spawnArgs.GetInt( "blend_out", "0" ); + if ( customAnim.Length() ) { + playCustomCycle( customAnim.c_str(), node->spawnArgs.GetInt( "blend_in", "0" ) ); + } + scratchEndTime = node->spawnArgs.GetFloat( "wait", "0" ) > 0.0f ? DelayTime( node->spawnArgs.GetFloat( "wait" ) ) : 0.0f; + ClearActivated(); + parms->stage = STAGE_HOLD; + return SRESULT_WAIT; + case STAGE_HOLD: + if ( owner && owner->AI_TALK ) { + if ( customAnim.Length() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( customBlendOut ) ); + } + cancelPathCommand(); + return SRESULT_DONE; + } + check_cower(); + LookAtEntity( focus, 0.1f ); + if ( Activated() || ( scratchEndTime > 0.0f && Time() >= scratchEndTime ) ) { + if ( customAnim.Length() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( customBlendOut ) ); + } + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharacter::state_FollowAlternatePath( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + endHeadAnim( 8.0f ); + executePathCommand( current_path.GetEntity() ); + parms->stage = 1; + } + if ( InAction() ) { + return SRESULT_WAIT; + } + SetState( "state_Idle" ); + return SRESULT_DONE; +} + +void rvmCharacter::Anim_Disable() { +} + +void rvmCharacter::check_cower() { + if ( !noCower && HeardSound( false ) ) { + endHeadAnim( 6.0f ); + cancelPathCommand(); + SetState( "state_Cower" ); + } +} + +void rvmCharacter::target_talk() { + cancelPathCommand(); + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + StartAction( "idle_talk" ); + SetState( "state_Idle" ); +} + +void rvmCharacter::say_triggered() { + cancelPathCommand(); + talkMode = CHAR_TALK_TRIGGERED; + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + SetState( "state_Idle" ); +} + +void rvmCharacter::say_primary() { + cancelPathCommand(); + talkMode = CHAR_TALK_PRIMARY; + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + SetState( "state_Idle" ); +} + +void rvmCharacter::say_secondary() { + cancelPathCommand(); + talkMode = CHAR_TALK_SECONDARY; + if ( owner ) { + owner->SetTalkTarget( Player() ); + } + SetState( "state_Idle" ); +} + +void rvmCharacter::faceTowardsEntity( idEntity* target, float duration ) { + if ( !target || !owner ) { + return; + } + idVec3 delta = target->GetPhysics()->GetOrigin() - GetOrigin(); + idAngles targetAngles = delta.ToAngles(); + const idVec3 currentAngles = owner->GetAngles(); + const float yawDelta = idMath::AngleNormalize180( targetAngles.yaw - currentAngles.y ); + if ( yawDelta < -CHAR_TURN_THRESHOLD || yawDelta > CHAR_TURN_THRESHOLD ) { + TurnTo( targetAngles.yaw ); + } + LookAtEntity( target, duration ); +} + +void rvmCharacter::playCustomCycle( const char* animname, float blendTime ) { + customAnim = animname ? animname : ""; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_CustomCycle", idMath::Ftoi( blendTime ) ); +} + +void rvmCharacter::playCustomAnim( const char* animname, float blendIn, float blendOut ) { + customBlendOut = blendOut; + customAnim = animname ? animname : ""; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_CustomAnim", idMath::Ftoi( blendIn ) ); +} + +bool rvmCharacter::inCustomAnim() { + return InAnimState( ANIMCHANNEL_LEGS, "Legs_CustomAnim" ); +} + +void rvmCharacter::endCustomAnim() { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( customBlendOut ) ); +} + +void rvmCharacter::waitForCustomAnim( const char* animname, float blendIn, float blendOut ) { + check_cower(); + customBlendOut = blendOut; + customAnim = animname ? animname : ""; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_CustomAnim", idMath::Ftoi( blendIn ) ); +} + +void rvmCharacter::endHeadAnim( float blend_out ) { + StopAnim( ANIMCHANNEL_HEAD, idMath::Ftoi( blend_out ) ); + SetBlendFrames( ANIMCHANNEL_LEGS, idMath::Ftoi( blend_out ) ); + IdleAnim( ANIMCHANNEL_LEGS, "stand" ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( blend_out ) ); + SetAnimState( ANIMCHANNEL_HEAD, "Head_Idle", idMath::Ftoi( blend_out ) ); +} + +float rvmCharacter::playHeadAnim( const char* animname, float blend_frames ) { + SetAnimState( ANIMCHANNEL_LEGS, "Anim_Disable", idMath::Ftoi( blend_frames ) ); + SetBlendFrames( ANIMCHANNEL_LEGS, idMath::Ftoi( blend_frames ) ); + IdleAnim( ANIMCHANNEL_LEGS, "stand" ); + + const char* headAnim = ChooseAnim( ANIMCHANNEL_HEAD, animname ); + if ( headAnim[0] ) { + SetBlendFrames( ANIMCHANNEL_HEAD, idMath::Ftoi( blend_frames ) ); + PlayAnim( ANIMCHANNEL_HEAD, headAnim ); + const float headLength = AnimLength( ANIMCHANNEL_HEAD, headAnim ); + + const char* bodyAnim = ChooseAnim( ANIMCHANNEL_LEGS, animname ); + if ( !bodyAnim[0] ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", idMath::Ftoi( blend_frames ) ); + } else if ( AnimLength( ANIMCHANNEL_LEGS, bodyAnim ) < headLength ) { + PlayCycle( ANIMCHANNEL_LEGS, bodyAnim ); + } else { + PlayAnim( ANIMCHANNEL_LEGS, bodyAnim ); + } + return ANIMCHANNEL_HEAD; + } + + if ( blend_frames >= 0.0f ) { + SetBlendFrames( ANIMCHANNEL_LEGS, idMath::Ftoi( blend_frames ) ); + } + const char* bodyAnim = ChooseAnim( ANIMCHANNEL_LEGS, animname ); + PlayAnim( ANIMCHANNEL_LEGS, bodyAnim[0] ? bodyAnim : "stand" ); + return ANIMCHANNEL_LEGS; +} + +void rvmCharacter::idle_followPathEntities( idEntity* pathnode ) { + current_path = pathnode; + StartAction( "idle_followPathEntities" ); +} + +void rvmCharacter::executePathCommand( idEntity* pathnode ) { + current_path = pathnode; + next_path = idPathCorner::RandomPath( current_path.GetEntity(), NULL ); + if ( current_path.GetEntity() ) { + const char* classname = current_path.GetEntity()->spawnArgs.GetString( "classname", current_path.GetEntity()->GetEntityDefName() ); + StartAction( classname ); + } +} + +void rvmCharacter::finishPathCommand() { + idEntity* node = current_path.GetEntity(); + if ( node ) { + const char* triggername = node->spawnArgs.GetString( "trigger", "" ); + if ( triggername[0] ) { + idEntity* trigger = gameLocal.FindEntity( triggername ); + if ( trigger ) { + trigger->ActivateTargets( owner ); + } + } + } + current_path = next_path; + skipOutOfPath = false; + if ( canTalk && owner ) { + owner->SetTalkState( TALK_OK ); + } + noCower = noCowerSaved; +} + +void rvmCharacter::cancelPathCommand() { + if ( skipOutOfPath ) { + finishPathCommand(); + } + skipOutOfPath = false; + if ( canTalk && owner ) { + owner->SetTalkState( TALK_OK ); + } + noCower = noCowerSaved; +} + +bool rvmCharacter::getOutOfWay() { + if ( ignorePush || !owner ) { + return false; + } + + if ( ( Time() - lastPushTime ) > FrameTime() ) { + lastPushTime = Time(); + pushTime = lastPushTime + 0.35f; + return false; + } + + lastPushTime = Time() + FrameTime(); + if ( Time() < pushTime ) { + return false; + } + + idVec3 org = owner->PushPointIntoNAV( GetOrigin() ); + for ( float yaw = 0.0f; yaw < 360.0f; yaw += 45.0f ) { + idAngles angles( 0.0f, yaw, 0.0f ); + idVec3 pos = owner->PushPointIntoNAV( org + angles.ToForward() * 124.0f ); + if ( TestMoveToPosition( pos ) ) { + MoveToPosition( pos ); + TurnTo( CurrentYaw() + 180.0f ); + return true; + } + } + return false; +} + +void rvmCharacter::skip_conversation() { + while ( current_path.GetEntity() ) { + const char* classname = current_path.GetEntity()->spawnArgs.GetString( "classname", current_path.GetEntity()->GetEntityDefName() ); + if ( idStr::Cmpn( classname, "path_conversation", 17 ) ) { + break; + } + current_path = next_path; + if ( next_path.GetEntity() ) { + next_path = idPathCorner::RandomPath( next_path.GetEntity(), NULL ); + } else { + next_path = NULL; + } + } + if ( owner ) { + owner->StopSound( SND_CHANNEL_VOICE, false ); + } +} + +void rvmCharacter::break_out_of_conversation() { + while ( next_listener.GetEntity() ) { + idAI* listenerAI = next_listener.GetEntity()->IsType( idAI::Type ) ? static_cast( next_listener.GetEntity() ) : NULL; + rvmCharacter* listener = listenerAI && listenerAI->nativeAIObject ? listenerAI->nativeAIObject->Cast() : NULL; + if ( listener ) { + next_listener = listener->next_listener; + listener->skip_conversation(); + listener->next_listener = NULL; + listener->listening_to_character = NULL; + } else { + next_listener = NULL; + } + } + skip_conversation(); +} + +void rvmCharacter::follow_alternate_path1() { + current_path = GetEntityKey( "alt_path1" ); + SetState( "state_FollowAlternatePath" ); +} + +void rvmCharacter::follow_alternate_path2() { + current_path = GetEntityKey( "alt_path2" ); + SetState( "state_FollowAlternatePath" ); +} + +void rvmCharacter::follow_alternate_path3() { + current_path = GetEntityKey( "alt_path3" ); + SetState( "state_FollowAlternatePath" ); +} + +void rvmCharacter::disable_turning() { + allowTurn = false; +} + +void rvmCharacter::enable_turning() { + allowTurn = true; +} diff --git a/neo/doom3/game/ai/AI_character.h b/neo/doom3/game/ai/AI_character.h new file mode 100644 index 00000000..c677e0a8 --- /dev/null +++ b/neo/doom3/game/ai/AI_character.h @@ -0,0 +1,116 @@ +// AI_character.h +// Native state-machine replacement for ai_character.script. + +#pragma once + + +class rvmCharacter : public rvmAIObject { +public: + CLASS_PROTOTYPE( rvmCharacter ); + + rvmCharacter(); + virtual void Init( idAI* aiOwner ) override; + virtual void Destroy() override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + enum { + CHAR_TALK_TRIGGERED = 0, + CHAR_TALK_PRIMARY = 1, + CHAR_TALK_SECONDARY = 2 + }; + + float talkMode; + int talkSecondaryIndex; + idStr customAnim; + float customBlendOut; + idStr walkAnim; + bool allowTurn; + bool run; + bool canTalk; + bool noCower; + bool noCowerSaved; + bool ignorePush; + bool skipOutOfPath; + bool followNodes; + bool dontPushOthers; + float lastPushTime; + float pushTime; + float talkEndTime; + float talkChannel; + float talkOriginalYaw; + idEntityPtr current_path; + idEntityPtr next_path; + idEntityPtr listening_to_character; + idEntityPtr next_listener; + + virtual stateResult_t Head_Idle( stateParms_t* parms ); + virtual stateResult_t Anim_Disable( stateParms_t* parms ); + virtual stateResult_t Head_Dead( stateParms_t* parms ); + virtual stateResult_t Head_TalkHeadOnly( stateParms_t* parms ); + virtual stateResult_t Legs_Death( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Walk_Special( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_TurnLeft( stateParms_t* parms ); + virtual stateResult_t Legs_TurnRight( stateParms_t* parms ); + virtual stateResult_t Legs_Cower( stateParms_t* parms ); + virtual stateResult_t Legs_CustomCycle( stateParms_t* parms ); + virtual stateResult_t Legs_CustomAnim( stateParms_t* parms ); + virtual stateResult_t state_Spawner( stateParms_t* parms ); + virtual stateResult_t state_Cower( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_TalkTrigger( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t idle_stand( stateParms_t* parms ); + virtual stateResult_t idle_followPathEntities( stateParms_t* parms ); + virtual stateResult_t idle_talk( stateParms_t* parms ); + virtual stateResult_t checkBlocked( stateParms_t* parms ); + virtual stateResult_t path_corner( stateParms_t* parms ); + virtual stateResult_t path_anim( stateParms_t* parms ); + virtual stateResult_t path_cycleanim( stateParms_t* parms ); + virtual stateResult_t path_turn( stateParms_t* parms ); + virtual stateResult_t path_wait( stateParms_t* parms ); + virtual stateResult_t path_waitfortrigger( stateParms_t* parms ); + virtual stateResult_t path_hide( stateParms_t* parms ); + virtual stateResult_t path_show( stateParms_t* parms ); + virtual stateResult_t path_conversation_listen( stateParms_t* parms ); + virtual stateResult_t path_conversation( stateParms_t* parms ); + virtual stateResult_t path_headanim( stateParms_t* parms ); + virtual stateResult_t path_waitforheadanim( stateParms_t* parms ); + virtual stateResult_t path_talk( stateParms_t* parms ); + virtual stateResult_t path_talk_triggered( stateParms_t* parms ); + virtual stateResult_t path_talk_primary( stateParms_t* parms ); + virtual stateResult_t path_talk_secondary( stateParms_t* parms ); + virtual stateResult_t path_lookat( stateParms_t* parms ); + virtual stateResult_t state_FollowAlternatePath( stateParms_t* parms ); + + virtual void Anim_Disable( ); + virtual void check_cower( ); + virtual void target_talk( ); + virtual void say_triggered( ); + virtual void say_primary( ); + virtual void say_secondary( ); + virtual void faceTowardsEntity( idEntity* target, float duration ); + virtual void playCustomCycle( const char* animname, float blendTime ); + virtual void playCustomAnim( const char* animname, float blendIn, float blendOut ); + virtual bool inCustomAnim( ); + virtual void endCustomAnim( ); + virtual void waitForCustomAnim( const char* animname, float blendIn, float blendOut ); + virtual void endHeadAnim( float blend_out ); + virtual float playHeadAnim( const char* animname, float blend_frames ); + virtual void idle_followPathEntities( idEntity* pathnode ); + virtual void executePathCommand( idEntity* pathnode ); + virtual void finishPathCommand( ); + virtual void cancelPathCommand( ); + virtual bool getOutOfWay( ); + virtual void skip_conversation( ); + virtual void break_out_of_conversation( ); + virtual void follow_alternate_path1( ); + virtual void follow_alternate_path2( ); + virtual void follow_alternate_path3( ); + virtual void disable_turning( ); + virtual void enable_turning( ); +}; diff --git a/neo/doom3/game/ai/AI_character_prone.cpp b/neo/doom3/game/ai/AI_character_prone.cpp new file mode 100644 index 00000000..ab4ae77a --- /dev/null +++ b/neo/doom3/game/ai/AI_character_prone.cpp @@ -0,0 +1,160 @@ +// AI_character_prone.cpp +// Native state-machine replacement for ai_character_prone.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmAIObject, rvmAICharacterProne ) +END_CLASS + +void rvmAICharacterProne::Init( idAI* aiOwner ) { + rvmAIObject::Init( aiOwner ); + SetState( "state_Begin" ); +} + +void rvmAICharacterProne::Destroy() { + rvmAIObject::Destroy(); +} + +stateResult_t rvmAICharacterProne::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Dead", Torso_Dead ); + AI_DISPATCH_STATE( "Torso_PlayDead", Torso_PlayDead ); + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_TalkPrimary", Torso_TalkPrimary ); + AI_DISPATCH_STATE( "Torso_TalkTrigger", Torso_TalkTrigger ); + AI_DISPATCH_STATE( "Torso_TalkSecondary", Torso_TalkSecondary ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "state_Pain", state_Pain ); + AI_DISPATCH_STATE( "state_Dead", state_Dead ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "idle_talk", idle_talk ); + return rvmAIObject::RunStateByName( stateName, parms ); +} + +stateResult_t rvmAICharacterProne::Torso_Dead( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAICharacterProne::Torso_PlayDead( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAICharacterProne::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmAICharacterProne::Torso_TalkPrimary( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAICharacterProne::Torso_TalkTrigger( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAICharacterProne::Torso_TalkSecondary( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAICharacterProne::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { FinishAction( "pain" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAICharacterProne::state_Idle( stateParms_t* parms ) { + if ( GetEnemy() ) { SetState( "state_Combat" ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmAICharacterProne::state_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_HEAD, GetPainAnim() ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_HEAD, 4 ) ) { FinishAction( "pain" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmAICharacterProne::state_Dead( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmAICharacterProne::state_Killed( stateParms_t* parms ) { + StopMove(); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 0 ); + StopThinking(); + return SRESULT_DONE; +} + +stateResult_t rvmAICharacterProne::idle_talk( stateParms_t* parms ) { + return SRESULT_DONE; +} diff --git a/neo/doom3/game/ai/AI_character_prone.h b/neo/doom3/game/ai/AI_character_prone.h new file mode 100644 index 00000000..dc7a68a3 --- /dev/null +++ b/neo/doom3/game/ai/AI_character_prone.h @@ -0,0 +1,28 @@ +// AI_character_prone.h +// Native state-machine replacement for ai_character_prone.script. + +#pragma once + + +class rvmAICharacterProne : public rvmAIObject { +public: + CLASS_PROTOTYPE( rvmAICharacterProne ); + + virtual void Init( idAI* aiOwner ) override; + virtual void Destroy() override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + virtual stateResult_t Torso_Dead( stateParms_t* parms ); + virtual stateResult_t Torso_PlayDead( stateParms_t* parms ); + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_TalkPrimary( stateParms_t* parms ); + virtual stateResult_t Torso_TalkTrigger( stateParms_t* parms ); + virtual stateResult_t Torso_TalkSecondary( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t state_Pain( stateParms_t* parms ); + virtual stateResult_t state_Dead( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t idle_talk( stateParms_t* parms ); +}; diff --git a/neo/doom3/game/ai/AI_character_sentry.cpp b/neo/doom3/game/ai/AI_character_sentry.cpp new file mode 100644 index 00000000..a5caffab --- /dev/null +++ b/neo/doom3/game/ai/AI_character_sentry.cpp @@ -0,0 +1,1043 @@ +// AI_character_sentry.cpp +// Native state-machine replacement for ai_character_sentry.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +namespace { + const float SENTRY_WAIT_GETCLOSER = 3.0f; + const float SENTRY_LEAD_TIMEOUT = 1.0f; + const float SENTRY_FIRERATE = 0.1f; + const int SENTRY_MIN_SHOTS = 8; + const float SENTRY_MIN_LOOK_DELAY = 4.0f; + const float SENTRY_MAX_LOOK_DELAY = 8.0f; + const float SENTRY_LOOK_TIME = 1.5f; + const float SENTRY_LEAD_DIST = 140.0f; + const float SENTRY_LOST_DIST = 280.0f; + const float SENTRY_WALKTURN = 65.0f; + const float SENTRY_WALKTURN2 = 30.0f; + const float SENTRY_ATTACK_MAX_LENGTH = 4.0f; + const float SENTRY_ATTACK_MIN_LENGTH = 1.2f; + const float SENTRY_WAIT_MAX_LENGTH = 1.0f; + const float SENTRY_WAIT_MIN_LENGTH = 0.3f; + const float SENTRY_MIN_TURN = 10.0f; +} + +CLASS_DECLARATION( rvmCharacter, rvmCharSentry ) +END_CLASS + +rvmCharSentry::rvmCharSentry() { + lead_player = false; + hanging = false; + awake = false; + fire = false; + playerMoveState = NOT_MOVING; + light_on = false; + nextPositionSearch = 0.0f; + ignore_enemy_time = 0.0f; + lastValidPlayerPosition = vec3_zero; + playerPositionThread = false; + nextLookTime = 0.0f; + leadTimeout = 0.0f; + shotsFired = 0; + nextFireTime = 0.0f; +} + +void rvmCharSentry::Init( idAI* aiOwner ) { + rvmCharacter::Init( aiOwner ); + + lead_player = GetIntKey( "lead_player" ) != 0; + canTalk = false; + noCowerSaved = true; + noCower = true; + ignore_enemy = NULL; + ignore_enemy_time = 0.0f; + lastValidPlayerPosition = Player() ? Player()->GetPhysics()->GetOrigin() : GetOrigin(); + SetState( "state_Begin" ); +} + +void rvmCharSentry::Destroy() { + if ( light.GetEntity() ) { + light.GetEntity()->PostEventMS( &EV_Remove, 0 ); + light = NULL; + } + playerPositionThread = false; + rvmCharacter::Destroy(); +} + +stateResult_t rvmCharSentry::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Death", Torso_Death ); + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Legs_Death", Legs_Death ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Fold", Legs_Fold ); + AI_DISPATCH_STATE( "Legs_Folded", Legs_Folded ); + AI_DISPATCH_STATE( "Legs_Unfold", Legs_Unfold ); + AI_DISPATCH_STATE( "Legs_Hanging", Legs_Hanging ); + AI_DISPATCH_STATE( "Legs_TurnLeft", Legs_TurnLeft ); + AI_DISPATCH_STATE( "Legs_TurnRight", Legs_TurnRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "updatePlayerPositionThread", updatePlayerPositionThread ); + AI_DISPATCH_STATE( "path_sentry_light_on", path_sentry_light_on ); + AI_DISPATCH_STATE( "path_sentry_light_off", path_sentry_light_off ); + AI_DISPATCH_STATE( "path_sentry_shutdown", path_sentry_shutdown ); + AI_DISPATCH_STATE( "path_sentry_unlock_door", path_sentry_unlock_door ); + AI_DISPATCH_STATE( "path_corner", path_corner ); + AI_DISPATCH_STATE( "path_sentry_lead_player", path_sentry_lead_player ); + AI_DISPATCH_STATE( "path_sentry_ignore_player", path_sentry_ignore_player ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "state_WaitForTrigger", state_WaitForTrigger ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "state_FollowPath", state_FollowPath ); + AI_DISPATCH_STATE( "state_Wait", state_Wait ); + AI_DISPATCH_STATE( "state_GetCloser", state_GetCloser ); + AI_DISPATCH_STATE( "state_CantReachPlayer", state_CantReachPlayer ); + AI_DISPATCH_STATE( "state_Lead", state_Lead ); + AI_DISPATCH_STATE( "state_FindPlayer", state_FindPlayer ); + AI_DISPATCH_STATE( "state_Combat", state_Combat ); + AI_DISPATCH_STATE( "combat_chase", combat_chase ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + return rvmCharacter::RunStateByName( stateName, parms ); +} + +stateResult_t rvmCharSentry::Torso_Death( stateParms_t* parms ) { + FinishAction( "dead" ); + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "stand" ); + parms->stage = 1; + } + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); + return SRESULT_DONE; + } + if ( fire ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::Torso_Pain( stateParms_t* parms ) { + switch ( parms->stage ) { + case 0: + SetBlendFrames( ANIMCHANNEL_TORSO, 2 ); + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + parms->stage = 1; + return SRESULT_WAIT; + case 1: + if ( !AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + return SRESULT_WAIT; + } + PreventPain( 2.0f ); + FinishAction( "pain" ); + SetBlendFrames( ANIMCHANNEL_TORSO, 4 ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharSentry::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_START_ANIM, STAGE_LOOP_BEGIN, STAGE_LOOP, STAGE_AIM, STAGE_END }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimPrefix( "" ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + shotsFired = 0; + parms->stage = STAGE_START_ANIM; + return SRESULT_WAIT; + case STAGE_START_ANIM: + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); + return SRESULT_DONE; + } + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { + return SRESULT_WAIT; + } + parms->stage = STAGE_LOOP_BEGIN; + return SRESULT_WAIT; + case STAGE_LOOP_BEGIN: + if ( !fire && shotsFired >= SENTRY_MIN_SHOTS ) { + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END; + return SRESULT_WAIT; + } + scratchEndTime = RandomDelay( SENTRY_ATTACK_MIN_LENGTH, SENTRY_ATTACK_MAX_LENGTH ); + nextFireTime = Time(); + SetBlendFrames( ANIMCHANNEL_TORSO, 2 ); + PlayCycle( ANIMCHANNEL_TORSO, "range_attack_loop" ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); + return SRESULT_DONE; + } + if ( ( fire || shotsFired < SENTRY_MIN_SHOTS ) && Time() < scratchEndTime ) { + if ( Time() >= nextFireTime ) { + StartSoundKey( "snd_fire", SND_CHANNEL_WEAPON, false ); + AttackMissile( "Barrel" ); + shotsFired++; + nextFireTime = Time() + SENTRY_FIRERATE; + } + return SRESULT_WAIT; + } + if ( !fire && shotsFired >= SENTRY_MIN_SHOTS ) { + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END; + return SRESULT_WAIT; + } + SetBlendFrames( ANIMCHANNEL_TORSO, 2 ); + PlayCycle( ANIMCHANNEL_TORSO, "range_attack_aim" ); + scratchEndTime = RandomDelay( SENTRY_WAIT_MIN_LENGTH, SENTRY_WAIT_MAX_LENGTH ); + parms->stage = STAGE_AIM; + return SRESULT_WAIT; + case STAGE_AIM: + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); + return SRESULT_DONE; + } + if ( ( fire || shotsFired < SENTRY_MIN_SHOTS ) && Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + parms->stage = STAGE_LOOP_BEGIN; + return SRESULT_WAIT; + case STAGE_END: + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); + return SRESULT_DONE; + } + if ( !AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + return SRESULT_WAIT; + } + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharSentry::Legs_Death( stateParms_t* parms ) { + if ( owner && owner->AI_DEAD ) { + return SRESULT_WAIT; + } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_LEGS, "stand" ); + parms->stage = 1; + } + if ( !awake ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Fold", 4 ); + return SRESULT_DONE; + } + if ( MovingForward() ) { + const float delta = GetTurnDelta(); + if ( delta <= SENTRY_WALKTURN && delta >= -SENTRY_WALKTURN ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 12 ); + return SRESULT_DONE; + } + } + if ( !FacingIdeal() ) { + if ( GetTurnDelta() > SENTRY_MIN_TURN ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_TurnLeft", 4 ); + return SRESULT_DONE; + } + if ( GetTurnDelta() < -SENTRY_MIN_TURN ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_TurnRight", 4 ); + return SRESULT_DONE; + } + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayCycle( ANIMCHANNEL_LEGS, "walk" ); + parms->stage = 1; + } + const float delta = GetTurnDelta(); + if ( !MovingForward() || delta > SENTRY_WALKTURN || delta < -SENTRY_WALKTURN ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 12 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::Legs_Fold( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + StartSoundKey( "snd_shutdown", SND_CHANNEL_VOICE, false ); + PlayAnim( ANIMCHANNEL_LEGS, "fold" ); + parms->stage = 1; + } + if ( !AnimDone( ANIMCHANNEL_LEGS, 0 ) ) { + return SRESULT_WAIT; + } + flashlight_off(); + FinishAction( "fold" ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Folded", 0 ); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::Legs_Folded( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayCycle( ANIMCHANNEL_LEGS, "folded" ); + parms->stage = 1; + } + if ( awake ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Unfold", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::Legs_Unfold( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayAnim( ANIMCHANNEL_LEGS, "unfold" ); + parms->stage = 1; + } + if ( !AnimDone( ANIMCHANNEL_LEGS, 8 ) ) { + return SRESULT_WAIT; + } + FinishAction( "unfold" ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::Legs_Hanging( stateParms_t* parms ) { + switch ( parms->stage ) { + case 0: + PlayCycle( ANIMCHANNEL_LEGS, "releaseidle" ); + parms->stage = 1; + return SRESULT_WAIT; + case 1: + if ( hanging ) { + return SRESULT_WAIT; + } + PlayAnim( ANIMCHANNEL_LEGS, "load" ); + parms->stage = 2; + return SRESULT_WAIT; + case 2: + if ( !AnimDone( ANIMCHANNEL_LEGS, 8 ) ) { + return SRESULT_WAIT; + } + FinishAction( "unload" ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharSentry::Legs_TurnLeft( stateParms_t* parms ) { + switch ( parms->stage ) { + case 0: + PlayAnim( ANIMCHANNEL_LEGS, "turn_left" ); + parms->stage = 1; + return SRESULT_WAIT; + case 1: + if ( MovingForward() && GetTurnDelta() <= SENTRY_WALKTURN2 && GetTurnDelta() >= -SENTRY_WALKTURN2 ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 12 ); + return SRESULT_DONE; + } + if ( !AnimDone( ANIMCHANNEL_LEGS, 0 ) ) { + return SRESULT_WAIT; + } + PlayAnim( ANIMCHANNEL_LEGS, "turn_right" ); + parms->stage = 2; + return SRESULT_WAIT; + case 2: + if ( MovingForward() && GetTurnDelta() <= SENTRY_WALKTURN2 && GetTurnDelta() >= -SENTRY_WALKTURN2 ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); + return SRESULT_DONE; + } + if ( !AnimDone( ANIMCHANNEL_LEGS, 8 ) ) { + return SRESULT_WAIT; + } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharSentry::Legs_TurnRight( stateParms_t* parms ) { + switch ( parms->stage ) { + case 0: + PlayAnim( ANIMCHANNEL_LEGS, "turn_right" ); + parms->stage = 1; + return SRESULT_WAIT; + case 1: + if ( MovingForward() && GetTurnDelta() <= SENTRY_WALKTURN2 && GetTurnDelta() >= -SENTRY_WALKTURN2 ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 12 ); + return SRESULT_DONE; + } + if ( !AnimDone( ANIMCHANNEL_LEGS, 0 ) ) { + return SRESULT_WAIT; + } + PlayAnim( ANIMCHANNEL_LEGS, "turn_left" ); + parms->stage = 2; + return SRESULT_WAIT; + case 2: + if ( MovingForward() && GetTurnDelta() <= SENTRY_WALKTURN2 && GetTurnDelta() >= -SENTRY_WALKTURN2 ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); + return SRESULT_DONE; + } + if ( !AnimDone( ANIMCHANNEL_LEGS, 8 ) ) { + return SRESULT_WAIT; + } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharSentry::state_Begin( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + fire = false; + if ( owner ) { + owner->SetJointMod( true ); + } + light_on = false; + if ( GetIntKey( "flashlight" ) ) { + spawn_flashlight(); + } + hanging = GetIntKey( "hanging" ) != 0; + awake = GetIntKey( "unfolded" ) != 0; + parms->stage = 1; + return SRESULT_WAIT; + } + + if ( hanging ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Hanging", 0 ); + } else if ( !awake ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Folded", 0 ); + } else { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + } + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetState( "state_WaitForTrigger" ); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::updatePlayerPositionThread( stateParms_t* parms ) { + if ( Player() && CanReachEntity( Player() ) ) { + lastValidPlayerPosition = GetReachableEntityPosition( Player() ); + } + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::path_sentry_light_on( stateParms_t* parms ) { + flashlight_on(); + finishPathCommand(); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::path_sentry_light_off( stateParms_t* parms ) { + flashlight_off(); + finishPathCommand(); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::path_sentry_shutdown( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + switch ( parms->stage ) { + case 0: + StopMove(); + if ( node ) { + TurnTo( node->GetPhysics()->GetAxis()[0].ToYaw() ); + } + parms->stage = 1; + return SRESULT_WAIT; + case 1: + if ( !FacingIdeal() ) { + return SRESULT_WAIT; + } + awake = false; + ClearActivated(); + playerPositionThread = false; + waitActionName = "fold"; + parms->stage = 2; + return SRESULT_WAIT; + case 2: + if ( !Activated() ) { + return SRESULT_WAIT; + } + if ( GetIntKey( "flashlight_on" ) ) { + flashlight_on(); + } + awake = true; + waitActionName = "unfold"; + parms->stage = 3; + return SRESULT_WAIT; + case 3: + if ( !ActionDone( "unfold" ) ) { + return SRESULT_WAIT; + } + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharSentry::path_sentry_unlock_door( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + switch ( parms->stage ) { + case 0: + if ( !node->spawnArgs.GetString( "trigger", "" )[0] ) { + finishPathCommand(); + return SRESULT_DONE; + } + TurnTo( node->GetPhysics()->GetAxis()[0].ToYaw() ); + parms->stage = 1; + return SRESULT_WAIT; + case 1: + if ( !FacingIdeal() ) { + return SRESULT_WAIT; + } + StartSoundKey( "snd_open_door", SND_CHANNEL_VOICE, false ); + playCustomAnim( "unlock_door", 4.0f, 4.0f ); + parms->stage = 2; + return SRESULT_WAIT; + case 2: + if ( inCustomAnim() ) { + return SRESULT_WAIT; + } + if ( idEntity* trigger = gameLocal.FindEntity( node->spawnArgs.GetString( "trigger", "" ) ) ) { + trigger->ActivateTargets( owner ); + } + node->spawnArgs.Set( "trigger", "" ); + finishPathCommand(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharSentry::path_corner( stateParms_t* parms ) { + if ( !lead_player ) { + SetState( "state_FollowPath" ); + return SRESULT_DONE; + } + SetNeverDormant( true ); + if ( Player() && !CanSeeEntity( Player() ) ) { + SetState( "state_FindPlayer" ); + } else if ( checkDestinationDistance() == PLAYER_BEHIND_ME ) { + SetState( "state_Wait" ); + } else { + SetState( "state_Lead" ); + } + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::path_sentry_lead_player( stateParms_t* parms ) { + lead_player = true; + playerPositionThread = true; + updatePlayerPositionThread( parms ); + finishPathCommand(); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::path_sentry_ignore_player( stateParms_t* parms ) { + lead_player = false; + playerPositionThread = false; + finishPathCommand(); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::state_Killed( stateParms_t* parms ) { + StopMove(); + playerPositionThread = false; + flashlight_off(); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 0 ); + if ( parms->stage == 0 ) { + waitActionName = "dead"; + scratchEndTime = GetFloatKey( "burnaway" ); + parms->stage = 1; + } + if ( scratchEndTime > 0.0f ) { + PreBurn(); + scratchEndTime = 0.0f; + } + StopThinking(); + return SRESULT_DONE; +} + +stateResult_t rvmCharSentry::state_WaitForTrigger( stateParms_t* parms ) { + switch ( parms->stage ) { + case 0: + if ( !GetIntKey( "hanging" ) || GetIntKey( "hanging" ) != 2 ) { + ClearActivated(); + parms->stage = 1; + return SRESULT_WAIT; + } + parms->stage = 2; + return SRESULT_WAIT; + case 1: + if ( !Activated() ) { + return SRESULT_WAIT; + } + parms->stage = 2; + return SRESULT_WAIT; + case 2: + if ( GetIntKey( "flashlight_on" ) ) { + flashlight_on(); + } + if ( hanging ) { + hanging = false; + awake = true; + waitActionName = "unload"; + parms->stage = 3; + return SRESULT_WAIT; + } + if ( !awake ) { + awake = true; + waitActionName = "unfold"; + parms->stage = 3; + return SRESULT_WAIT; + } + parms->stage = 3; + return SRESULT_WAIT; + case 3: + if ( waitActionName.Length() && !ActionDone( waitActionName.c_str() ) ) { + return SRESULT_WAIT; + } + AllowDamage(); + LookAtEntity( Player(), 3.0f ); + current_path = owner ? owner->RandomPath() : NULL; + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmCharSentry::state_Idle( stateParms_t* parms ) { + StopMove(); + if ( lead_player ) { + playerPositionThread = true; + updatePlayerPositionThread( parms ); + if ( Player() && !CanSeeEntity( Player() ) ) { + SetState( "state_FindPlayer" ); + return SRESULT_DONE; + } + } + if ( checkForEnemy( true ) ) { + SetState( "state_Combat" ); + return SRESULT_DONE; + } + if ( owner && owner->AI_PUSHED ) { + getOutOfWay(); + owner->AI_PUSHED = false; + } + if ( current_path.GetEntity() ) { + executePathCommand( current_path.GetEntity() ); + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::state_FollowPath( stateParms_t* parms ) { + if ( !current_path.GetEntity() ) { + SetState( "state_Idle" ); + return SRESULT_DONE; + } + if ( parms->stage == 0 ) { + MoveToEntity( current_path.GetEntity() ); + parms->stage = 1; + } + if ( checkForEnemy( true ) ) { + SetState( "state_Combat" ); + return SRESULT_DONE; + } + checkBlocked( parms ); + if ( MoveDone() ) { + if ( DestinationUnreachable() ) { + SetState( "state_FindPlayer" ); + return SRESULT_DONE; + } + SetNeverDormant( GetFloatKey( "neverdormant" ) != 0.0f ); + finishPathCommand(); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::state_Wait( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + StopMove(); + StartSoundKey( "snd_waiting_for_player", SND_CHANNEL_VOICE, false ); + scratchEndTime = DelayTime( SENTRY_WAIT_GETCLOSER ); + parms->stage = 1; + } + if ( checkForEnemy( true ) ) { + SetState( "state_Combat" ); + return SRESULT_DONE; + } + LookAtEntity( Player(), 0.1f ); + if ( owner && owner->AI_PUSHED ) { + getOutOfWay(); + owner->AI_PUSHED = false; + } + const float result = checkDestinationDistance(); + if ( result == PLAYER_LOST ) { + SetState( "state_FindPlayer" ); + } else if ( result == PLAYER_AHEAD_OF_ME || ( result == PLAYER_OK && playerMoveState == APPROACHING ) ) { + SetState( "state_Lead" ); + } else if ( Time() > scratchEndTime && result == PLAYER_BEHIND_ME ) { + SetState( "state_GetCloser" ); + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::state_GetCloser( stateParms_t* parms ) { + MoveToPosition( lastValidPlayerPosition ); + if ( MoveDone() && Player() && !CanReachEntity( Player() ) ) { + SetState( "state_CantReachPlayer" ); + return SRESULT_DONE; + } + if ( checkForEnemy( true ) ) { + SetState( "state_Combat" ); + return SRESULT_DONE; + } + checkBlocked( parms ); + const float result = checkDestinationDistance(); + if ( result == PLAYER_LOST ) { + SetState( "state_FindPlayer" ); + } else if ( result == PLAYER_OK ) { + SetState( "state_Wait" ); + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::state_CantReachPlayer( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + StartSoundKey( "snd_cant_reach_player", SND_CHANNEL_VOICE, false ); + StopMove(); + parms->stage = 1; + } + if ( Player() && CanReachEntity( Player() ) ) { + SetState( "state_Wait" ); + return SRESULT_DONE; + } + if ( checkForEnemy( true ) ) { + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::state_Lead( stateParms_t* parms ) { + if ( !current_path.GetEntity() ) { + SetState( "state_Idle" ); + return SRESULT_DONE; + } + if ( parms->stage == 0 ) { + nextLookTime = RandomDelay( SENTRY_MIN_LOOK_DELAY, SENTRY_MAX_LOOK_DELAY ); + leadTimeout = DelayTime( SENTRY_LEAD_TIMEOUT ); + MoveToEntity( current_path.GetEntity() ); + parms->stage = 1; + } + if ( checkForEnemy( true ) ) { + SetState( "state_Combat" ); + return SRESULT_DONE; + } + if ( !light_on && Time() > nextLookTime ) { + LookAtEntity( Player(), SENTRY_LOOK_TIME ); + nextLookTime = RandomDelay( SENTRY_MIN_LOOK_DELAY, SENTRY_MAX_LOOK_DELAY ); + } + checkBlocked( parms ); + const float result = checkDestinationDistance(); + if ( result != PLAYER_BEHIND_ME && result != PLAYER_LOST ) { + leadTimeout = DelayTime( SENTRY_LEAD_TIMEOUT ); + } else if ( Time() > leadTimeout ) { + SetState( "state_Wait" ); + return SRESULT_DONE; + } else if ( !light_on ) { + LookAtEntity( Player(), 0.1f ); + nextLookTime += 0.1f; + } + if ( MoveDone() ) { + if ( DestinationUnreachable() ) { + SetState( "state_FindPlayer" ); + return SRESULT_DONE; + } + SetNeverDormant( GetFloatKey( "neverdormant" ) != 0.0f ); + finishPathCommand(); + SetState( "state_Idle" ); + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::state_FindPlayer( stateParms_t* parms ) { + MoveToPosition( lastValidPlayerPosition ); + if ( ( MoveDone() || DestinationUnreachable() ) && Player() && !CanReachEntity( Player() ) ) { + SetState( "state_CantReachPlayer" ); + return SRESULT_DONE; + } + if ( checkForEnemy( true ) ) { + SetState( "state_Combat" ); + return SRESULT_DONE; + } + checkBlocked( parms ); + const float result = checkDestinationDistance(); + if ( Player() && CanSeeEntity( Player() ) && ( result == PLAYER_BEHIND_ME || result == PLAYER_OK ) ) { + SetState( "state_Wait" ); + } else if ( result == PLAYER_AHEAD_OF_ME ) { + SetState( "state_Lead" ); + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::state_Combat( stateParms_t* parms ) { + FaceEnemy(); + LookAtEnemy( 1.0f ); + if ( EnemyDead() || !GetEnemy() ) { + StartSoundKey( "snd_target_lost", SND_CHANNEL_VOICE, false ); + if ( owner ) { + owner->AI_ENEMY_DEAD = false; + } + ClearEnemy(); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + const int attackFlags = check_attacks(); + if ( attackFlags ) { + do_attack( attackFlags ); + return SRESULT_WAIT; + } + if ( CanReachEnemy() ) { + StartAction( "combat_chase" ); + } else if ( !find_attack_position() ) { + ignore( GetEnemy() ); + checkForEnemy( false ); + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::combat_chase( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + MoveToEnemy(); + parms->stage = 1; + } + if ( DestinationUnreachable() ) { + StopMove(); + return SRESULT_DONE; + } + if ( EnemyDead() ) { + StartSoundKey( "snd_target_lost", SND_CHANNEL_VOICE, false ); + if ( owner ) { + owner->AI_ENEMY_DEAD = false; + } + ClearEnemy(); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + if ( MoveDone() ) { + MoveToEnemy(); + } + LookAtEnemy( 1.0f ); + const int attackFlags = check_attacks(); + if ( attackFlags ) { + do_attack( attackFlags ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmCharSentry::combat_range( stateParms_t* parms ) { + switch ( parms->stage ) { + case 0: + FaceEnemy(); + fire = true; + parms->stage = 1; + return SRESULT_WAIT; + case 1: + if ( CanHitEnemyFromAnim( "range_attack_loop" ) ) { + return SRESULT_WAIT; + } + fire = false; + parms->stage = 2; + return SRESULT_WAIT; + case 2: + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack" ) ) { + return SRESULT_WAIT; + } + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +void rvmCharSentry::spawn_flashlight() { + if ( light.GetEntity() || !owner ) { + return; + } + + PlayAnim( ANIMCHANNEL_TORSO, "ik_pose" ); + + idDict args; + args.Set( "classname", "light" ); + args.Set( "name", va( "%s_light", owner->GetName() ) ); + if ( GetIntKey( "flashlight" ) == 2 ) { + const float distance = GetFloatKey( "flashlight_distance", 640.0f ); + args.Set( "light_target", "1 0 0" ); + args.Set( "light_up", "0 0 .5" ); + args.Set( "light_right", "0 -.5 0" ); + args.Set( "light_end", va( "%g 0 0", distance ) ); + args.Set( "texture", GetKey( "mtr_flashlight" ) ); + } + + idEntity* ent = NULL; + gameLocal.SpawnEntityDef( args, &ent ); + light = ent; + if ( ent ) { + const idVec3 ownerAngles = owner->GetAngles(); + ent->SetAngles( idAngles( ownerAngles.x, ownerAngles.y, ownerAngles.z ) ); + ent->BindToJoint( owner, "light", true ); + ent->SetOrigin( vec3_zero ); + if ( GetIntKey( "flashlight" ) != 2 && ent->IsType( idLight::Type ) ) { + static_cast( ent )->SetRadius( 256.0f ); + static_cast( ent )->SetShader( GetKey( "mtr_flashlight" ) ); + } + } + StopAnim( ANIMCHANNEL_TORSO, 0 ); + flashlight_off(); +} + +void rvmCharSentry::flashlight_off() { + if ( !light.GetEntity() ) { + return; + } + if ( light.GetEntity()->IsType( idLight::Type ) ) { + static_cast( light.GetEntity() )->Off(); + } + if ( owner ) { + owner->SetSkin( declManager->FindSkin( GetKey( "skin_flashlight_off" ) ) ); + } + light_on = false; +} + +void rvmCharSentry::flashlight_on() { + if ( !light.GetEntity() ) { + return; + } + if ( light.GetEntity()->IsType( idLight::Type ) ) { + static_cast( light.GetEntity() )->On(); + } + if ( owner ) { + owner->SetSkin( declManager->FindSkin( GetKey( "skin_flashlight_on" ) ) ); + } + light_on = true; +} + +void rvmCharSentry::ignore( idEntity* ignore_ent ) { + ignore_enemy = ignore_ent; + ignore_enemy_time = Time() + 2.0f; +} + +bool rvmCharSentry::checkForEnemy( float use_fov ) { + idEntity* enemy = NULL; + idPlayer* player = Player(); + + if ( lead_player && player && player->HasEnemies() ) { + enemy = ClosestReachableEnemyOfEntity( player ); + if ( ignore_enemy_time > Time() && ignore_enemy.GetEntity() == enemy ) { + enemy = NULL; + } + if ( !enemy ) { + enemy = player->ClosestEnemyToPoint( GetOrigin() ); + if ( ignore_enemy_time > Time() && ignore_enemy.GetEntity() == enemy ) { + enemy = NULL; + } + } + } + + if ( !enemy ) { + enemy = FindEnemyAI( use_fov != 0.0f ); + if ( ignore_enemy_time > Time() && ignore_enemy.GetEntity() == enemy ) { + enemy = NULL; + } + } + + if ( enemy ) { + StartSoundKey( "snd_sight_enemy", SND_CHANNEL_VOICE, false ); + SetEnemy( enemy ); + return true; + } + + ClearEnemy(); + return false; +} + +float rvmCharSentry::checkDestinationDistance() { + idPlayer* player = Player(); + if ( !player ) { + return PLAYER_OK; + } + + if ( CanReachEntity( player ) ) { + lastValidPlayerPosition = GetReachableEntityPosition( player ); + } + + idVec3 delta = GetOrigin() - lastValidPlayerPosition; + const float relativeMoveDir = delta * player->GetPhysics()->GetLinearVelocity(); + if ( relativeMoveDir > 1.0f ) { + playerMoveState = APPROACHING; + } else if ( relativeMoveDir < -1.0f ) { + playerMoveState = RETREATING; + } else { + playerMoveState = NOT_MOVING; + } + + if ( !current_path.GetEntity() ) { + return PLAYER_OK; + } + + const float playerDist = ( GetOrigin() - lastValidPlayerPosition ).Length(); + if ( playerDist < SENTRY_LEAD_DIST ) { + return PLAYER_OK; + } + + if ( !CanSeeEntity( player ) || playerDist > SENTRY_LOST_DIST ) { + const float currentDist = TravelDistanceToEntity( current_path.GetEntity() ); + const float leadDistance = TravelDistanceBetweenPoints( lastValidPlayerPosition, current_path.GetEntity()->GetPhysics()->GetOrigin() ); + if ( currentDist > leadDistance ) { + return PLAYER_AHEAD_OF_ME; + } + return CanSeeEntity( player ) ? PLAYER_BEHIND_ME : PLAYER_LOST; + } + + return PLAYER_OK; +} + +bool rvmCharSentry::find_attack_position() { + if ( Time() < nextPositionSearch || !GetEnemy() ) { + return false; + } + nextPositionSearch = Time() + 1.0f; + LocateEnemy(); + MoveToAttackPosition( GetEnemy(), "range_attack_loop" ); + return !DestinationUnreachable(); +} + +void rvmCharSentry::do_attack( int attack_flags ) { + if ( attack_flags & ATTACK_MISSILE ) { + StartAction( "combat_range" ); + } +} + +int rvmCharSentry::check_attacks() { + return CanHitEnemyFromAnim( "range_attack_loop" ) ? ATTACK_MISSILE : 0; +} diff --git a/neo/doom3/game/ai/AI_character_sentry.h b/neo/doom3/game/ai/AI_character_sentry.h new file mode 100644 index 00000000..278c7f55 --- /dev/null +++ b/neo/doom3/game/ai/AI_character_sentry.h @@ -0,0 +1,88 @@ +// AI_character_sentry.h +// Native state-machine replacement for ai_character_sentry.script. + +#pragma once + + +class rvmCharSentry : public rvmCharacter { +public: + CLASS_PROTOTYPE( rvmCharSentry ); + + rvmCharSentry(); + virtual void Init( idAI* aiOwner ) override; + virtual void Destroy() override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + enum { + RETREATING = -1, + NOT_MOVING = 0, + APPROACHING = 1, + PLAYER_LOST = 0, + PLAYER_BEHIND_ME = 1, + PLAYER_OK = 2, + PLAYER_AHEAD_OF_ME = 3 + }; + + bool lead_player; + bool hanging; + bool awake; + bool fire; + float playerMoveState; + idEntityPtr light; + bool light_on; + float nextPositionSearch; + idEntityPtr ignore_enemy; + float ignore_enemy_time; + idVec3 lastValidPlayerPosition; + bool playerPositionThread; + float nextLookTime; + float leadTimeout; + int shotsFired; + float nextFireTime; + + virtual stateResult_t Torso_Death( stateParms_t* parms ); + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Death( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Fold( stateParms_t* parms ); + virtual stateResult_t Legs_Folded( stateParms_t* parms ); + virtual stateResult_t Legs_Unfold( stateParms_t* parms ); + virtual stateResult_t Legs_Hanging( stateParms_t* parms ); + virtual stateResult_t Legs_TurnLeft( stateParms_t* parms ); + virtual stateResult_t Legs_TurnRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t updatePlayerPositionThread( stateParms_t* parms ); + virtual stateResult_t path_sentry_light_on( stateParms_t* parms ); + virtual stateResult_t path_sentry_light_off( stateParms_t* parms ); + virtual stateResult_t path_sentry_shutdown( stateParms_t* parms ); + virtual stateResult_t path_sentry_unlock_door( stateParms_t* parms ); + virtual stateResult_t path_corner( stateParms_t* parms ); + virtual stateResult_t path_sentry_lead_player( stateParms_t* parms ); + virtual stateResult_t path_sentry_ignore_player( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t state_WaitForTrigger( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t state_FollowPath( stateParms_t* parms ); + virtual stateResult_t state_Wait( stateParms_t* parms ); + virtual stateResult_t state_GetCloser( stateParms_t* parms ); + virtual stateResult_t state_CantReachPlayer( stateParms_t* parms ); + virtual stateResult_t state_Lead( stateParms_t* parms ); + virtual stateResult_t state_FindPlayer( stateParms_t* parms ); + virtual stateResult_t state_Combat( stateParms_t* parms ); + virtual stateResult_t combat_chase( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + + virtual void spawn_flashlight( ); + virtual void flashlight_off( ); + virtual void flashlight_on( ); + virtual void ignore( idEntity* ignore_ent ); + virtual bool checkForEnemy( float use_fov ); + virtual float checkDestinationDistance( ); + virtual bool find_attack_position( ); + virtual void do_attack( int attack_flags ); + virtual int check_attacks( ); +}; diff --git a/neo/doom3/game/ai/AI_follower.cpp b/neo/doom3/game/ai/AI_follower.cpp new file mode 100644 index 00000000..441d7fdc --- /dev/null +++ b/neo/doom3/game/ai/AI_follower.cpp @@ -0,0 +1,199 @@ +// AI_follower.cpp +// Native state-machine replacement for ai_follower(1).script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmAIObject, rvmFollower ) +END_CLASS + +void rvmFollower::Init( idAI* aiOwner ) { + rvmAIObject::Init( aiOwner ); + SetState( "state_Begin" ); +} + +void rvmFollower::Destroy() { + rvmAIObject::Destroy(); +} + +stateResult_t rvmFollower::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_CustomCycle", Torso_CustomCycle ); + AI_DISPATCH_STATE( "Torso_CustomAnim", Torso_CustomAnim ); + AI_DISPATCH_STATE( "Torso_Death", Torso_Death ); + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Legs_Death", Legs_Death ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_TurnLeft", Legs_TurnLeft ); + AI_DISPATCH_STATE( "Legs_TurnRight", Legs_TurnRight ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "state_Follow", state_Follow ); + AI_DISPATCH_STATE( "getCloser", getCloser ); + return rvmAIObject::RunStateByName( stateName, parms ); +} + +stateResult_t rvmFollower::Torso_CustomCycle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmFollower::Torso_CustomAnim( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmFollower::Torso_Death( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmFollower::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmFollower::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { FinishAction( "pain" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmFollower::Legs_Death( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmFollower::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "stand" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmFollower::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmFollower::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmFollower::Legs_TurnLeft( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmFollower::Legs_TurnRight( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ANIM }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmFollower::state_Idle( stateParms_t* parms ) { + if ( GetEnemy() ) { SetState( "state_Combat" ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmFollower::state_Killed( stateParms_t* parms ) { + StopMove(); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 0 ); + StopThinking(); + return SRESULT_DONE; +} + +stateResult_t rvmFollower::state_Follow( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmFollower::getCloser( stateParms_t* parms ) { + return SRESULT_DONE; +} + +float rvmFollower::should_turn_left( ) { + return 0.0f; +} + +float rvmFollower::should_turn_right( ) { + return 0.0f; +} + +void rvmFollower::playCustomCycle( const char* animname, float blendTime ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_CustomCycle", idMath::Ftoi( blendTime ) ); + PlayCycle( ANIMCHANNEL_TORSO, animname ); +} + +void rvmFollower::playCustomAnim( const char* animname, float blendIn, float blendOut ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_CustomAnim", idMath::Ftoi( blendIn ) ); + PlayAnim( ANIMCHANNEL_TORSO, animname ); +} diff --git a/neo/doom3/game/ai/AI_follower.h b/neo/doom3/game/ai/AI_follower.h new file mode 100644 index 00000000..06084066 --- /dev/null +++ b/neo/doom3/game/ai/AI_follower.h @@ -0,0 +1,36 @@ +// AI_follower.h +// Native state-machine replacement for ai_follower(1).script. + +#pragma once + + +class rvmFollower : public rvmAIObject { +public: + CLASS_PROTOTYPE( rvmFollower ); + + virtual void Init( idAI* aiOwner ) override; + virtual void Destroy() override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + virtual stateResult_t Torso_CustomCycle( stateParms_t* parms ); + virtual stateResult_t Torso_CustomAnim( stateParms_t* parms ); + virtual stateResult_t Torso_Death( stateParms_t* parms ); + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Legs_Death( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_TurnLeft( stateParms_t* parms ); + virtual stateResult_t Legs_TurnRight( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t state_Follow( stateParms_t* parms ); + virtual stateResult_t getCloser( stateParms_t* parms ); + + virtual float should_turn_left( ); + virtual float should_turn_right( ); + virtual void playCustomCycle( const char* animname, float blendTime ); + virtual void playCustomAnim( const char* animname, float blendIn, float blendOut ); +}; diff --git a/neo/doom3/game/ai/AI_monster_base.cpp b/neo/doom3/game/ai/AI_monster_base.cpp new file mode 100644 index 00000000..d2cdb627 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_base.cpp @@ -0,0 +1,1173 @@ +// AI_monster_base.cpp +// Native state-machine replacement for ai_monster_base.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmAIObject, rvmMonsterBase ) +END_CLASS + +rvmMonsterBase::rvmMonsterBase() { + run_distance = 0.0f; + walk_turn = 360.0f; + run = false; + ambush = false; + ignoreEnemies = false; + stay_on_attackpath = false; + idle_sight_fov = true; + customBlendOut = 0.0f; + current_path = NULL; + next_path = NULL; + resurrect = false; + ignore_lostcombat = false; + blocked = false; + ignore_sight = false; +} + +void rvmMonsterBase::Init( idAI* aiOwner ) { + rvmAIObject::Init( aiOwner ); + ignoreEnemies = false; + run_distance = 0.0f; + walk_turn = 360.0f; + ambush = GetIntKey( "ambush" ) != 0; + ignore_lostcombat = GetIntKey( "ignore_lostcombat" ) != 0; + stay_on_attackpath = GetIntKey( "stay_on_attackpath" ) != 0; + idle_sight_fov = true; + run = false; + blocked = false; + ignore_sight = false; +} + +stateResult_t rvmMonsterBase::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_CustomCycle", Torso_CustomCycle ); + AI_DISPATCH_STATE( "Torso_CustomAnim", Torso_CustomAnim ); + AI_DISPATCH_STATE( "Torso_Sight", Torso_Sight ); + AI_DISPATCH_STATE( "Torso_Death", Torso_Death ); + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Death", Legs_Death ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Spawner", state_Spawner ); + AI_DISPATCH_STATE( "state_Resurrect", state_Resurrect ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "state_Dead", state_Dead ); + AI_DISPATCH_STATE( "state_Combat", state_Combat ); + AI_DISPATCH_STATE( "state_LostCombat", state_LostCombat ); + AI_DISPATCH_STATE( "state_FollowAlternatePath", state_FollowAlternatePath ); + AI_DISPATCH_STATE( "monster_begin", monster_begin ); + AI_DISPATCH_STATE( "wait_for_enemy", wait_for_enemy ); + AI_DISPATCH_STATE( "sight_enemy", sight_enemy ); + AI_DISPATCH_STATE( "walk_on_trigger", walk_on_trigger ); + AI_DISPATCH_STATE( "wake_on_trigger", wake_on_trigger ); + AI_DISPATCH_STATE( "wake_on_enemy", wake_on_enemy ); + AI_DISPATCH_STATE( "wake_on_attackcone", wake_on_attackcone ); + AI_DISPATCH_STATE( "idle_followPathEntities", idle_followPathEntities ); + AI_DISPATCH_STATE( "path_corner", path_corner ); + AI_DISPATCH_STATE( "path_anim", path_anim ); + AI_DISPATCH_STATE( "path_cycleanim", path_cycleanim ); + AI_DISPATCH_STATE( "path_turn", path_turn ); + AI_DISPATCH_STATE( "path_wait", path_wait ); + AI_DISPATCH_STATE( "path_waitfortrigger", path_waitfortrigger ); + AI_DISPATCH_STATE( "path_hide", path_hide ); + AI_DISPATCH_STATE( "path_show", path_show ); + AI_DISPATCH_STATE( "path_attack", path_attack ); + AI_DISPATCH_STATE( "combat_wander", combat_wander ); + AI_DISPATCH_STATE( "combat_turret_node", combat_turret_node ); + AI_DISPATCH_STATE( "combat_attack_cone", combat_attack_cone ); + AI_DISPATCH_STATE( "combat_ainode", combat_ainode ); + return rvmAIObject::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterBase::Torso_CustomCycle( stateParms_t* parms ) { + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBase::Torso_CustomAnim( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, idMath::Ftoi( customBlendOut ) ) ) { + FinishAction( "customAnim" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", idMath::Ftoi( customBlendOut ) ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::Torso_Sight( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: { + if ( !GetIntKey( "walk_on_sight" ) ) { + OverrideAnim( ANIMCHANNEL_LEGS ); + } + const char* animname = GetKey( "on_activate" ); + if ( animname && animname[0] && HasAnim( ANIMCHANNEL_TORSO, animname ) ) { + PlayAnim( ANIMCHANNEL_TORSO, animname ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + } + FinishAction( "sight" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", GetIntKey( "on_activate_blend", "4" ) ); + return SRESULT_DONE; + } + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, GetIntKey( "on_activate_blend", "4" ) ) ) { + FinishAction( "sight" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", GetIntKey( "on_activate_blend", "4" ) ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::Torso_Death( stateParms_t* parms ) { + FinishAction( "dead" ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBase::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = 1; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBase::Legs_Idle( stateParms_t* parms ) { + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBase::Legs_Death( stateParms_t* parms ) { + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBase::state_Begin( stateParms_t* parms ) { + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBase::state_Spawner( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP }; + switch ( parms->stage ) { + case STAGE_START: + HideOwner(); + ClearActivated(); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( Activated() && CanBecomeSolid() ) { + ClearActivated(); + // Spawn-copy behavior is project-specific; keep this state non-blocking. + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::monster_begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_HIDDEN_WAIT_TRIGGER, STAGE_HIDDEN_WAIT_SOLID, STAGE_DELAY, STAGE_WAKE, STAGE_SIGHT }; + switch ( parms->stage ) { + case STAGE_START: + run = false; + ignore_sight = GetIntKey( "no_sight" ) != 0; + if ( !GetIntKey( "ignore_flashlight" ) ) { + WakeOnFlashlight( true ); + } + if ( GetIntKey( "hide" ) ) { + HideOwner(); + ClearActivated(); + parms->stage = STAGE_HIDDEN_WAIT_TRIGGER; + return SRESULT_WAIT; + } + scratchEndTime = DelayTime( GetFloatKey( "wait" ) ); + parms->stage = STAGE_DELAY; + return SRESULT_WAIT; + + case STAGE_HIDDEN_WAIT_TRIGGER: + if ( !Activated() && !Pain() ) { + return SRESULT_WAIT; + } + ClearActivated(); + parms->stage = STAGE_HIDDEN_WAIT_SOLID; + return SRESULT_WAIT; + + case STAGE_HIDDEN_WAIT_SOLID: + if ( !CanBecomeSolid() ) { + return SRESULT_WAIT; + } + ShowOwner(); + scratchEndTime = DelayTime( GetFloatKey( "wait" ) ); + parms->stage = STAGE_DELAY; + return SRESULT_WAIT; + + case STAGE_DELAY: + if ( Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + if ( idEntity* enemyEnt = GetEntityKey( "enemy" ) ) { + SetEnemy( enemyEnt ); + } + parms->stage = STAGE_WAKE; + return SRESULT_WAIT; + + case STAGE_WAKE: + if ( GetEnemy() || Pain() || Activated() || checkForEnemy( idle_sight_fov ) ) { + trigger_wakeup_targets(); + ignore_sight = false; + WakeOnFlashlight( false ); + waitActionName = "sight"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 ); + parms->stage = STAGE_SIGHT; + } + return SRESULT_WAIT; + + case STAGE_SIGHT: + if ( !ActionDone( "sight" ) ) { + return SRESULT_WAIT; + } + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::state_Resurrect( stateParms_t* parms ) { + enum { STAGE_START, STAGE_SOLID }; + switch ( parms->stage ) { + case STAGE_START: + if ( owner ) { + owner->AI_DEAD = false; + } + StopMove(); + HideOwner(); + if ( owner ) { + owner->StopRagdoll(); + // restorePosition() was a DoomScript helper; this idAI branch has no + // public RestorePosition() native. StopRagdoll() is the available + // native restore hook here. + } + parms->stage = STAGE_SOLID; + return SRESULT_WAIT; + case STAGE_SOLID: + if ( !CanBecomeSolid() ) { + return SRESULT_WAIT; + } + if ( owner ) { + owner->SetHealth( GetFloatKey( "health", "1" ) ); + owner->TurnTo( GetFloatKey( "angle" ) ); + owner->ClearBurn(); + owner->AllowDamage(); + } + SetState( "state_Begin" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::wait_for_enemy( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP }; + switch ( parms->stage ) { + case STAGE_START: + if ( owner ) { + owner->AI_PAIN = false; + } + StopMove(); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( Pain() || GetEnemy() || checkForEnemy( idle_sight_fov ) ) { + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::state_Killed( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_DEAD_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + waitActionName = "dead"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 0 ); + parms->stage = STAGE_WAIT_DEAD_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_DEAD_ACTION: + if ( !ActionDone( "dead" ) ) { + return SRESULT_WAIT; + } + SetState( "state_Dead" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::state_Dead( stateParms_t* parms ) { + enum { STAGE_START, STAGE_BURN_DELAY, STAGE_REMOVE_DELAY, STAGE_RESURRECT_HOLD }; + switch ( parms->stage ) { + case STAGE_START: + scratchEndTime = DelayTime( GetFloatKey( "burnaway" ) ); + if ( GetFloatKey( "burnaway" ) > 0.0f && owner ) { + owner->PreBurn(); + parms->stage = STAGE_BURN_DELAY; + } else { + scratchEndTime = DelayTime( 3.0f ); + parms->stage = STAGE_REMOVE_DELAY; + } + return SRESULT_WAIT; + case STAGE_BURN_DELAY: + if ( Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + if ( owner ) { + owner->Burn(); + StartSoundKey( "snd_burn", SND_CHANNEL_BODY ); + } + scratchEndTime = DelayTime( 3.0f ); + parms->stage = STAGE_REMOVE_DELAY; + return SRESULT_WAIT; + case STAGE_REMOVE_DELAY: + if ( Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + if ( resurrect ) { + HideOwner(); + if ( owner ) { + owner->StopRagdoll(); + // restorePosition() was a DoomScript helper; this idAI branch has no + // public RestorePosition() native. StopRagdoll() is the available + // native restore hook here. + } + parms->stage = STAGE_RESURRECT_HOLD; + return SRESULT_WAIT; + } + RemoveOwner(); + return SRESULT_DONE; + case STAGE_RESURRECT_HOLD: + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::sight_enemy( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + if ( GetKey( "on_activate" )[0] ) { + SetNeverDormant( true ); + if ( GetIntKey( "walk_on_sight" ) ) { + MoveToEnemy(); + } + waitActionName = "sight"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + } + return SRESULT_DONE; + case STAGE_WAIT: + if ( ActionDone( "sight" ) ) { + SetNeverDormant( GetFloatKey( "neverdormant" ) != 0.0f ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +void rvmMonsterBase::enemy_dead() { + if ( owner ) { + owner->AI_ENEMY_DEAD = false; + } + checkForEnemy( false ); + if ( !GetEnemy() ) { + SetState( "state_Idle" ); + } else { + SetState( "state_Combat" ); + } +} + +stateResult_t rvmMonsterBase::walk_on_trigger( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTIVATED, STAGE_DONE }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + playCustomCycle( GetKey( "anim" ), 4 ); + parms->stage = STAGE_WAIT_ACTIVATED; + return SRESULT_WAIT; + case STAGE_WAIT_ACTIVATED: + if ( !Activated() && !Pain() && !checkForEnemy( true ) ) { + return SRESULT_WAIT; + } + ClearActivated(); + AllowMovement( true ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + trigger_wakeup_targets(); + StartAction( "sight_enemy" ); + parms->stage = STAGE_DONE; + return SRESULT_WAIT; + case STAGE_DONE: + return InAction() ? SRESULT_WAIT : SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::wake_on_trigger( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTIVATED, STAGE_DONE }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + playCustomCycle( GetKey( "anim" ), 4 ); + parms->stage = STAGE_WAIT_ACTIVATED; + return SRESULT_WAIT; + case STAGE_WAIT_ACTIVATED: + if ( !Activated() && !Pain() ) { + return SRESULT_WAIT; + } + AllowMovement( true ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + trigger_wakeup_targets(); + StartAction( "sight_enemy" ); + parms->stage = STAGE_DONE; + return SRESULT_WAIT; + case STAGE_DONE: + return InAction() ? SRESULT_WAIT : SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::wake_on_enemy( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ENEMY, STAGE_DONE }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + playCustomCycle( GetKey( "anim" ), 4 ); + parms->stage = STAGE_WAIT_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_ENEMY: + if ( !Pain() && !Activated() && !checkForEnemy( true ) ) { + return SRESULT_WAIT; + } + AllowMovement( true ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + trigger_wakeup_targets(); + StartAction( "sight_enemy" ); + parms->stage = STAGE_DONE; + return SRESULT_WAIT; + case STAGE_DONE: + return InAction() ? SRESULT_WAIT : SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::wake_on_attackcone( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_CONE, STAGE_DONE }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + playCustomCycle( GetKey( "anim" ), 4 ); + parms->stage = STAGE_WAIT_CONE; + return SRESULT_WAIT; + case STAGE_WAIT_CONE: { + idEntity* enemyEnt = owner ? owner->FindEnemyInCombatNodes() : NULL; + if ( enemyEnt ) { + SetEnemy( enemyEnt ); + } + if ( !Activated() && !Pain() && !GetEnemy() ) { + return SRESULT_WAIT; + } + AllowMovement( true ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + trigger_wakeup_targets(); + StartAction( "sight_enemy" ); + parms->stage = STAGE_DONE; + return SRESULT_WAIT; + } + case STAGE_DONE: + return InAction() ? SRESULT_WAIT : SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::idle_followPathEntities( stateParms_t* parms ) { + // Path-entity execution is kept as a non-blocking state. The current node is advanced + // one frame at a time; unsupported nodes are skipped instead of suspending the VM. + enum { STAGE_START, STAGE_NODE, STAGE_NODE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + if ( !current_path.GetEntity() ) { + current_path = owner ? owner->RandomPath() : NULL; + } + parms->stage = STAGE_NODE; + return SRESULT_WAIT; + case STAGE_NODE: + if ( !current_path.GetEntity() ) { + return SRESULT_DONE; + } + next_path = idPathCorner::RandomPath( current_path.GetEntity(), NULL ); + // Resolve common node class names into action states. + scratchString = current_path.GetEntity()->GetEntityDefName(); + if ( !scratchString.Icmp( "path_corner" ) ) { + StartAction( "path_corner" ); + } else if ( !scratchString.Icmp( "path_anim" ) ) { + StartAction( "path_anim" ); + } else if ( !scratchString.Icmp( "path_cycleanim" ) ) { + StartAction( "path_cycleanim" ); + } else if ( !scratchString.Icmp( "path_turn" ) ) { + StartAction( "path_turn" ); + } else if ( !scratchString.Icmp( "path_wait" ) ) { + StartAction( "path_wait" ); + } else if ( !scratchString.Icmp( "path_waitfortrigger" ) ) { + StartAction( "path_waitfortrigger" ); + } else if ( !scratchString.Icmp( "path_hide" ) ) { + StartAction( "path_hide" ); + } else if ( !scratchString.Icmp( "path_show" ) ) { + StartAction( "path_show" ); + } else if ( !scratchString.Icmp( "path_attack" ) ) { + StartAction( "path_attack" ); + } else { + current_path = next_path; + return SRESULT_WAIT; + } + parms->stage = STAGE_NODE_ACTION; + return SRESULT_WAIT; + case STAGE_NODE_ACTION: + if ( InAction() ) { + return SRESULT_WAIT; + } + if ( checkForEnemy( true ) ) { + return SRESULT_DONE; + } + current_path = next_path; + parms->stage = STAGE_NODE; + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::path_corner( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MOVE }; + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + switch ( parms->stage ) { + case STAGE_START: + run = node->spawnArgs.GetBool( "run", run ? "1" : "0" ); + scratchString = node->spawnArgs.GetString( "anim", "" ); + if ( scratchString.Length() ) { + playCustomCycle( scratchString.c_str(), 4 ); + } + MoveToEntity( node ); + parms->stage = STAGE_MOVE; + return SRESULT_WAIT; + case STAGE_MOVE: + if ( InfluenceActive() ) { + StopMove(); + return SRESULT_WAIT; + } + if ( checkForEnemy( true ) ) { + return SRESULT_DONE; + } + if ( MoveDone() || DestinationUnreachable() ) { + if ( scratchString.Length() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + } + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::path_anim( stateParms_t* parms ) { + enum { STAGE_START, STAGE_TURN, STAGE_ANIM }; + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + switch ( parms->stage ) { + case STAGE_START: + scratchString = node->spawnArgs.GetString( "anim", "" ); + customBlendOut = node->spawnArgs.GetInt( "blend_out", "0" ); + if ( node->spawnArgs.GetString( "angle", "" )[0] ) { + TurnTo( node->spawnArgs.GetFloat( "angle" ) ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + } + playCustomAnim( scratchString.c_str(), node->spawnArgs.GetInt( "blend_in", "0" ), customBlendOut ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_TURN: + if ( checkForEnemy( true ) ) { + return SRESULT_DONE; + } + if ( !FacingIdeal() ) { + return SRESULT_WAIT; + } + playCustomAnim( scratchString.c_str(), node->spawnArgs.GetInt( "blend_in", "0" ), customBlendOut ); + parms->stage = STAGE_ANIM; + return SRESULT_WAIT; + case STAGE_ANIM: + if ( checkForEnemy( true ) || AnimDone( ANIMCHANNEL_TORSO, idMath::Ftoi( customBlendOut ) ) ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", idMath::Ftoi( customBlendOut ) ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::path_cycleanim( stateParms_t* parms ) { + enum { STAGE_START, STAGE_TURN, STAGE_HOLD }; + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + switch ( parms->stage ) { + case STAGE_START: + scratchString = node->spawnArgs.GetString( "anim", "" ); + customBlendOut = node->spawnArgs.GetInt( "blend_out", "0" ); + TurnTo( node->GetPhysics()->GetAxis()[0].ToYaw() ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + case STAGE_TURN: + if ( checkForEnemy( true ) ) { + return SRESULT_DONE; + } + if ( !FacingIdeal() ) { + return SRESULT_WAIT; + } + playCustomCycle( scratchString.c_str(), node->spawnArgs.GetInt( "blend_in", "0" ) ); + scratchEndTime = node->spawnArgs.GetFloat( "wait", "0" ) > 0.0f ? DelayTime( node->spawnArgs.GetFloat( "wait" ) ) : 0.0f; + ClearActivated(); + parms->stage = STAGE_HOLD; + return SRESULT_WAIT; + case STAGE_HOLD: + if ( checkForEnemy( true ) ) { + return SRESULT_DONE; + } + if ( scratchEndTime > 0.0f ) { + if ( Time() < scratchEndTime ) { + return SRESULT_WAIT; + } + } else if ( !Activated() ) { + return SRESULT_WAIT; + } + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", idMath::Ftoi( customBlendOut ) ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::path_turn( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + if ( parms->stage == 0 ) { + TurnTo( node->GetPhysics()->GetAxis()[0].ToYaw() ); + parms->stage = 1; + } + if ( checkForEnemy( true ) || FacingIdeal() ) { + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBase::path_wait( stateParms_t* parms ) { + idEntity* node = current_path.GetEntity(); + if ( !node ) { + return SRESULT_DONE; + } + if ( parms->stage == 0 ) { + scratchEndTime = DelayTime( node->spawnArgs.GetFloat( "wait", "0" ) ); + parms->stage = 1; + } + if ( checkForEnemy( true ) || Time() >= scratchEndTime ) { + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBase::path_waitfortrigger( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + ClearActivated(); + parms->stage = 1; + } + if ( checkForEnemy( true ) ) { + return SRESULT_DONE; + } + if ( Activated() ) { + ClearActivated(); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBase::path_hide( stateParms_t* parms ) { + HideOwner(); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBase::path_show( stateParms_t* parms ) { + if ( !CanBecomeSolid() ) { + return SRESULT_WAIT; + } + ShowOwner(); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBase::path_attack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_COMBAT }; + idEntity* node = current_path.GetEntity(); + switch ( parms->stage ) { + case STAGE_START: + if ( !node ) { + return SRESULT_DONE; + } + scratchEntity = node->spawnArgs.GetString( "enemy", "" )[0] ? gameLocal.FindEntity( node->spawnArgs.GetString( "enemy" ) ) : NULL; + if ( !scratchEntity.GetEntity() ) { + return SRESULT_DONE; + } + SetEnemy( scratchEntity.GetEntity() ); + LocateEnemy(); + ClearActivated(); + parms->stage = STAGE_COMBAT; + return SRESULT_WAIT; + case STAGE_COMBAT: + if ( Activated() || !scratchEntity.GetEntity() || EnemyDead() ) { + StopMove(); + return SRESULT_DONE; + } + if ( combat_chase() ) { + return SRESULT_WAIT; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::state_Combat( stateParms_t* parms ) { + FaceEnemy(); + if ( EnemyInFOV() ) { + LookAtEnemy( 1.0f ); + } + + if ( InfluenceActive() ) { + return SRESULT_WAIT; + } + + if ( EnemyDead() ) { + enemy_dead(); + return SRESULT_DONE; + } + + const int attack_flags = check_attacks(); + if ( attack_flags != 0 && do_attack( attack_flags ) ) { + return SRESULT_WAIT; + } + + if ( !combat_chase() ) { + LocateEnemy(); + if ( !combat_chase() ) { + combat_lost(); + } + } + + return SRESULT_WAIT; +} + +bool rvmMonsterBase::check_blocked() { + if ( Blocked() ) { + Wander(); + return false; + } + + if ( MoveStatus() == MOVE_STATUS_BLOCKED_BY_OBJECT && owner ) { + owner->KickObstacles( owner->GetObstacle(), GetFloatKey( "kick_force", "60" ) ); + } + return false; +} + +bool rvmMonsterBase::combat_chase() { + bool do_run = ( !EnemyVisible() || EnemyRange() > run_distance ); + MoveToEnemy(); + if ( MoveDone() || DestinationUnreachable() ) { + return false; + } + + if ( EnemyDead() ) { + enemy_dead(); + return true; + } + + if ( EnemyInFOV() ) { + LookAtEnemy( 1.0f ); + } + + const int attack_flags = check_attacks(); + if ( attack_flags != 0 && do_attack( attack_flags ) ) { + return true; + } + + if ( check_blocked() ) { + return true; + } + + if ( !EnemyVisible() || EnemyRange() > run_distance ) { + do_run = true; + } + + const float delta = GetTurnDelta(); + run = !(( delta > walk_turn ) || ( delta < -walk_turn )) && do_run; + return true; +} + +bool rvmMonsterBase::checkTurretAttack() { + return CanHitEnemy(); +} + +stateResult_t rvmMonsterBase::combat_turret_node( stateParms_t* parms ) { + // Long turret-node script is represented as a resumable attack-node state. + // It reaches the node, runs the subclass turret animation, and releases movement. + enum { STAGE_START, STAGE_MOVE, STAGE_ATTACK }; + idEntity* node = scratchEntity.GetEntity(); + switch ( parms->stage ) { + case STAGE_START: + if ( !node ) { + return SRESULT_DONE; + } + AllowMovement( true ); + run = true; + MoveToEntity( node ); + parms->stage = STAGE_MOVE; + return SRESULT_WAIT; + case STAGE_MOVE: + if ( !node || DestinationUnreachable() || CheckForMelee() || EnemyDead() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + AllowMovement( true ); + return SRESULT_DONE; + } + if ( !MoveDone() ) { + if ( EnemyInFOV() ) { + LookAtEnemy( 1.0f ); + } + return SRESULT_WAIT; + } + FaceEnemy(); + AllowMovement( false ); + waitActionName = "turret_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_TurretAttack", 4 ); + parms->stage = STAGE_ATTACK; + return SRESULT_WAIT; + case STAGE_ATTACK: + if ( !ActionDone( "turret_attack" ) ) { + if ( EnemyInFOV() ) { + LookAtEnemy( 1.0f ); + } + return SRESULT_WAIT; + } + AllowMovement( true ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +void rvmMonsterBase::checkNodeAnim( idEntity* node, const char* prefix, const char* anim ) { + idStr fullName = prefix; + fullName += "_"; + fullName += anim; + if ( !HasAnim( ANIMCHANNEL_TORSO, fullName.c_str() ) && owner && node ) { + gameLocal.Warning( "AI node '%s' specifies missing anim '%s' on monster '%s'", node->GetName(), fullName.c_str(), owner->GetName() ); + } +} + +stateResult_t rvmMonsterBase::combat_attack_cone( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MOVE, STAGE_WAIT, STAGE_OUT, STAGE_FIRE, STAGE_IN }; + idEntity* node = scratchEntity.GetEntity(); + switch ( parms->stage ) { + case STAGE_START: + if ( !node ) { + return SRESULT_DONE; + } + run = true; + MoveToEntity( node ); + parms->stage = STAGE_MOVE; + return SRESULT_WAIT; + case STAGE_MOVE: + if ( !node || DestinationUnreachable() || CheckForMelee() || EnemyDead() || ( owner && !owner->EnemyInCombatCone( node, false ) ) ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + if ( !MoveDone() ) { + if ( EnemyInFOV() ) { + LookAtEnemy( 1.0f ); + } + return SRESULT_WAIT; + } + scratchString = node->spawnArgs.GetString( "anim", "" ); + if ( !scratchString.Length() ) { + return SRESULT_DONE; + } + SetAnimPrefix( scratchString.c_str() ); + checkNodeAnim( node, scratchString.c_str(), "out" ); + checkNodeAnim( node, scratchString.c_str(), "fire" ); + checkNodeAnim( node, scratchString.c_str(), "in" ); + checkNodeAnim( node, scratchString.c_str(), "wait" ); + TurnTo( node->GetPhysics()->GetAxis()[0].ToYaw() ); + AllowMovement( false ); + playCustomCycle( "wait", 4 ); + scratchEndTime = DelayTime( node->spawnArgs.GetFloat( "min_wait", "0" ) + RandomFloat( node->spawnArgs.GetFloat( "max_wait", "0" ) - node->spawnArgs.GetFloat( "min_wait", "0" ) ) ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( !node || CheckForMelee() || EnemyDead() || ( owner && !owner->EnemyInCombatCone( node, false ) ) ) { + parms->stage = STAGE_IN; + return SRESULT_WAIT; + } + if ( Time() < scratchEndTime || !FacingIdeal() ) { + if ( EnemyInFOV() ) { + LookAtEnemy( 1.0f ); + } + return SRESULT_WAIT; + } + AllowMovement( true ); + playCustomAnim( "out", 4, 0 ); + parms->stage = STAGE_OUT; + return SRESULT_WAIT; + case STAGE_OUT: + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { + return SRESULT_WAIT; + } + AllowMovement( false ); + playCustomAnim( "fire", 0, 0 ); + scratchCount = idMath::Ftoi( RandomFloat( node ? node->spawnArgs.GetFloat( "num_shots", "1" ) : 1.0f ) ) + 1; + parms->stage = STAGE_FIRE; + return SRESULT_WAIT; + case STAGE_FIRE: + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { + return SRESULT_WAIT; + } + scratchCount -= 1.0f; + if ( scratchCount > 0.0f && node && !EnemyDead() ) { + playCustomAnim( "fire", 0, 0 ); + return SRESULT_WAIT; + } + AllowMovement( true ); + playCustomAnim( "in", 0, 4 ); + parms->stage = STAGE_IN; + return SRESULT_WAIT; + case STAGE_IN: + if ( !AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + return SRESULT_WAIT; + } + AllowMovement( true ); + SetAnimPrefix( "" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::combat_ainode( stateParms_t* parms ) { + idEntity* node = scratchEntity.GetEntity(); + if ( node && !idStr::Icmp( node->spawnArgs.GetString( "classname" ), "ai_attackcone_turret" ) ) { + return combat_turret_node( parms ); + } + return combat_attack_cone( parms ); +} + +void rvmMonsterBase::combat_lost() { + if ( !ignore_lostcombat ) { + SetState( "state_LostCombat" ); + } +} + +stateResult_t rvmMonsterBase::state_LostCombat( stateParms_t* parms ) { + monster_base_lost_combat(); + return SRESULT_WAIT; +} + +void rvmMonsterBase::monster_base_lost_combat() { + if ( EnemyVisible() || InfluenceActive() ) { + SetState( "state_Combat" ); + } else { + ClearEnemy(); + SetState( "state_Idle" ); + } +} + +stateResult_t rvmMonsterBase::combat_wander( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WANDER }; + switch ( parms->stage ) { + case STAGE_START: + scratchMinTime = DelayTime( 0.2f ); + scratchEndTime = DelayTime( 3.0f ); + Wander(); + parms->stage = STAGE_WANDER; + return SRESULT_WAIT; + case STAGE_WANDER: + if ( InfluenceActive() || check_attacks() || Time() >= scratchEndTime || ( Time() > scratchMinTime && CanReachEnemy() && ( EnemyInFOV() || Pain() ) ) ) { + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBase::state_FollowAlternatePath( stateParms_t* parms ) { + enum { STAGE_START, STAGE_PATH }; + switch ( parms->stage ) { + case STAGE_START: + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_CustomCycle" ) || InAnimState( ANIMCHANNEL_TORSO, "Torso_CustomAnim" ) ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + } + ignoreEnemies = true; + StartAction( "idle_followPathEntities" ); + parms->stage = STAGE_PATH; + return SRESULT_WAIT; + case STAGE_PATH: + if ( InAction() ) { + return SRESULT_WAIT; + } + ignoreEnemies = false; + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterBase::check_attacks() { + return 0; +} + +bool rvmMonsterBase::do_attack( int attack_flags ) { + return false; +} + +void rvmMonsterBase::playCustomCycle( const char* animname, float blendTime ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_CustomCycle", idMath::Ftoi( blendTime ) ); + OverrideAnim( ANIMCHANNEL_LEGS ); + PlayCycle( ANIMCHANNEL_TORSO, animname ); +} + +void rvmMonsterBase::playCustomAnim( const char* animname, float blendIn, float blendOut ) { + customBlendOut = blendOut; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_CustomAnim", idMath::Ftoi( blendIn ) ); + OverrideAnim( ANIMCHANNEL_LEGS ); + PlayAnim( ANIMCHANNEL_TORSO, animname ); +} + +void rvmMonsterBase::trigger_wakeup_targets() { + if ( !owner ) { + return; + } + const idKeyValue* kv = owner->spawnArgs.MatchPrefix( "wakeup_target", NULL ); + while ( kv ) { + idEntity* ent = gameLocal.FindEntity( kv->GetValue() ); + if ( ent ) { + ent->ProcessEvent( &EV_Activate, owner ); + } else { + gameLocal.Warning( "Unknown wakeup_target '%s' on entity '%s'", kv->GetValue().c_str(), owner->GetName() ); + } + kv = owner->spawnArgs.MatchPrefix( "wakeup_target", kv ); + } +} + +bool rvmMonsterBase::checkForEnemy( bool use_fov ) { + if ( InfluenceActive() ) { + return false; + } + + if ( Pain() ) { + ambush = false; + } + + idEntity* enemyEnt = NULL; + if ( ignoreEnemies ) { + if ( stay_on_attackpath ) { + return false; + } + enemyEnt = GetEnemy(); + if ( !enemyEnt && owner ) { + enemyEnt = owner->FindEnemy( false ); + } + if ( !enemyEnt ) { + return false; + } + const idBounds& b = owner->GetPhysics()->GetBounds(); + const float dist = ( ( b[1] - b[0] ).x * 1.414f ) + 16.0f; + if ( EnemyRange() > dist ) { + return false; + } + } else { + if ( GetEnemy() ) { + return true; + } + if ( !ignore_sight && owner ) { + enemyEnt = owner->FindEnemy( use_fov ? 1 : 0 ); + } + if ( !enemyEnt ) { + if ( ambush ) { + return false; + } + enemyEnt = owner ? owner->HeardSound( true ) : NULL; + if ( !enemyEnt ) { + return false; + } + } + } + + ignoreEnemies = false; + ambush = false; + idle_sight_fov = false; + SetEnemy( enemyEnt ); + return true; +} + +void rvmMonsterBase::archvile_minion() { + HideOwner(); + resurrect = true; + if ( owner ) { + owner->AI_DEAD = true; + } +} + +bool rvmMonsterBase::can_resurrect() { + return owner && owner->AI_DEAD && owner->IsHidden() && CanBecomeSolid(); +} + +void rvmMonsterBase::monster_resurrect( idEntity* enemyEnt ) { + if ( owner ) { + owner->AI_DEAD = false; + } + SetEnemy( enemyEnt ? enemyEnt : Player() ); + SetState( "state_Resurrect" ); +} + +void rvmMonsterBase::follow_alternate_path1() { + current_path = GetEntityKey( "alt_path1" ); + SetState( "state_FollowAlternatePath" ); +} + +void rvmMonsterBase::follow_alternate_path2() { + current_path = GetEntityKey( "alt_path2" ); + SetState( "state_FollowAlternatePath" ); +} + +void rvmMonsterBase::follow_alternate_path3() { + current_path = GetEntityKey( "alt_path3" ); + SetState( "state_FollowAlternatePath" ); +} diff --git a/neo/doom3/game/ai/AI_monster_base.h b/neo/doom3/game/ai/AI_monster_base.h new file mode 100644 index 00000000..5aabced2 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_base.h @@ -0,0 +1,92 @@ +// AI_monster_base.h +// Native state-machine replacement for ai_monster_base.script. + +#pragma once + + +class rvmMonsterBase : public rvmAIObject { +public: + CLASS_PROTOTYPE( rvmMonsterBase ); + + rvmMonsterBase(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + float run_distance; + float walk_turn; + bool run; + bool ambush; + bool ignoreEnemies; + bool stay_on_attackpath; + bool idle_sight_fov; + float customBlendOut; + idEntityPtr current_path; + idEntityPtr next_path; + bool resurrect; + bool ignore_lostcombat; + bool blocked; + bool ignore_sight; + + // Animation state machines. + virtual stateResult_t Torso_CustomCycle( stateParms_t* parms ); + virtual stateResult_t Torso_CustomAnim( stateParms_t* parms ); + virtual stateResult_t Torso_Sight( stateParms_t* parms ); + virtual stateResult_t Torso_Death( stateParms_t* parms ); + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Death( stateParms_t* parms ); + + // Main/action state machines. + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Spawner( stateParms_t* parms ); + virtual stateResult_t state_Resurrect( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t state_Dead( stateParms_t* parms ); + virtual stateResult_t state_Combat( stateParms_t* parms ); + virtual stateResult_t state_LostCombat( stateParms_t* parms ); + virtual stateResult_t state_FollowAlternatePath( stateParms_t* parms ); + + virtual stateResult_t monster_begin( stateParms_t* parms ); + virtual stateResult_t wait_for_enemy( stateParms_t* parms ); + virtual stateResult_t sight_enemy( stateParms_t* parms ); + virtual stateResult_t walk_on_trigger( stateParms_t* parms ); + virtual stateResult_t wake_on_trigger( stateParms_t* parms ); + virtual stateResult_t wake_on_enemy( stateParms_t* parms ); + virtual stateResult_t wake_on_attackcone( stateParms_t* parms ); + virtual stateResult_t idle_followPathEntities( stateParms_t* parms ); + virtual stateResult_t path_corner( stateParms_t* parms ); + virtual stateResult_t path_anim( stateParms_t* parms ); + virtual stateResult_t path_cycleanim( stateParms_t* parms ); + virtual stateResult_t path_turn( stateParms_t* parms ); + virtual stateResult_t path_wait( stateParms_t* parms ); + virtual stateResult_t path_waitfortrigger( stateParms_t* parms ); + virtual stateResult_t path_hide( stateParms_t* parms ); + virtual stateResult_t path_show( stateParms_t* parms ); + virtual stateResult_t path_attack( stateParms_t* parms ); + virtual stateResult_t combat_wander( stateParms_t* parms ); + virtual stateResult_t combat_turret_node( stateParms_t* parms ); + virtual stateResult_t combat_attack_cone( stateParms_t* parms ); + virtual stateResult_t combat_ainode( stateParms_t* parms ); + + // Non-blocking helpers. + virtual int check_attacks(); + virtual bool do_attack( int attack_flags ); + virtual bool checkTurretAttack(); + virtual void playCustomCycle( const char* animname, float blendTime ); + virtual void playCustomAnim( const char* animname, float blendIn, float blendOut ); + virtual void trigger_wakeup_targets(); + virtual bool checkForEnemy( bool use_fov ); + virtual void archvile_minion(); + virtual bool can_resurrect(); + virtual void monster_resurrect( idEntity* enemy ); + virtual void enemy_dead(); + virtual bool check_blocked(); + virtual bool combat_chase(); + virtual void combat_lost(); + virtual void monster_base_lost_combat(); + virtual void checkNodeAnim( idEntity* node, const char* prefix, const char* anim ); + virtual void follow_alternate_path1(); + virtual void follow_alternate_path2(); + virtual void follow_alternate_path3(); +}; diff --git a/neo/doom3/game/ai/AI_monster_boss_cyberdemon.cpp b/neo/doom3/game/ai/AI_monster_boss_cyberdemon.cpp new file mode 100644 index 00000000..da61720a --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_cyberdemon.cpp @@ -0,0 +1,208 @@ +// AI_monster_boss_cyberdemon.cpp + // Native state-machine replacement for ai_monster_boss_cyberdemon.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterBossCyberdemon ) + END_CLASS + + rvmMonsterBossCyberdemon::rvmMonsterBossCyberdemon() { + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + painNextTime = 0.0f; + kick_entity = NULL; +} + +void rvmMonsterBossCyberdemon::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + nextNoFOVAttack = 0.0f; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterBossCyberdemon::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterBossCyberdemon::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( ( owner && owner->AI_SPECIAL_DAMAGE ) ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", CYBER_IDLE_TO_PAIN ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossCyberdemon::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( CYBER_PAIN_DELAY ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( ( owner && owner->AI_SPECIAL_DAMAGE ) && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", CYBER_PAIN_TO_PAIN ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, CYBER_PAIN_TO_IDLE ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", CYBER_PAIN_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossCyberdemon::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 0.1f ); + if ( AnimDone( ANIMCHANNEL_TORSO, CYBER_MELEE_TO_IDLE ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", CYBER_MELEE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossCyberdemon::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + PlayAnim( ANIMCHANNEL_TORSO, range_attack_anim.c_str() ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 1.0f ); + if ( AnimDone( ANIMCHANNEL_TORSO, CYBER_RANGE_TO_IDLE ) ) { + AllowMovement( true ); + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", CYBER_RANGE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossCyberdemon::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "idle" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", CYBER_IDLE_TO_WALK ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossCyberdemon::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", CYBER_WALK_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossCyberdemon::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossCyberdemon::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterBossCyberdemon::check_attacks() { + int flags = 0; + if ( EnemyVisible() ) { + const idVec3 org = GetOrigin(); + kick_entity = FindActorsInBounds( org + idVec3( -65, -65, 0 ), org + idVec3( 65, 65, 300 ) ); + if ( kick_entity.GetEntity() ) { flags |= ATTACK_MELEE; } + } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + if ( !CanReachEnemy() || Time() >= nextAttack ) { + range_attack_anim = ChooseAnim( ANIMCHANNEL_LEGS, "range_attack" ); + if ( CanHitEnemyFromAnim( range_attack_anim.c_str() ) ) { flags |= ATTACK_MISSILE; } + } + } + return static_cast( flags ); +} + +bool rvmMonsterBossCyberdemon::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + CYBER_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterBossCyberdemon::combat_range( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + StopMove(); + waitActionName = "range_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", CYBER_WALK_TO_RANGEATTACK ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "range_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( CYBER_ATTACK_RATE ); + nextNoFOVAttack = Time() + CYBER_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossCyberdemon::combat_melee( stateParms_t* parms ) { + enum { STAGE_KICK, STAGE_DAMAGE }; + switch ( parms->stage ) { + case STAGE_KICK: + if ( owner ) { owner->KickObstacles( kick_entity.GetEntity(), 200.0f ); } + scratchEndTime = DelayTime( 0.5f ); + parms->stage = STAGE_DAMAGE; + return SRESULT_WAIT; + case STAGE_DAMAGE: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + DirectDamage( kick_entity.GetEntity(), "melee_cyberdemon_kick" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_boss_cyberdemon.h b/neo/doom3/game/ai/AI_monster_boss_cyberdemon.h new file mode 100644 index 00000000..f7526de3 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_cyberdemon.h @@ -0,0 +1,72 @@ +// AI_monster_boss_cyberdemon.h + // Native state-machine replacement for ai_monster_boss_cyberdemon.script. + + #pragma once + + + #ifndef CYBER_ATTACK_RATE +#define CYBER_ATTACK_RATE 5 +#endif +#ifndef CYBER_PAIN_DELAY +#define CYBER_PAIN_DELAY 0.25f +#endif +#ifndef CYBER_NOFOVTIME +#define CYBER_NOFOVTIME 4 +#endif +#ifndef CYBER_PAIN_TO_IDLE +#define CYBER_PAIN_TO_IDLE 2 +#endif +#ifndef CYBER_PAIN_TO_PAIN +#define CYBER_PAIN_TO_PAIN 0 +#endif +#ifndef CYBER_MELEE_TO_IDLE +#define CYBER_MELEE_TO_IDLE 4 +#endif +#ifndef CYBER_RANGE_TO_IDLE +#define CYBER_RANGE_TO_IDLE 4 +#endif +#ifndef CYBER_IDLE_TO_PAIN +#define CYBER_IDLE_TO_PAIN 0 +#endif +#ifndef CYBER_IDLE_TO_WALK +#define CYBER_IDLE_TO_WALK 4 +#endif +#ifndef CYBER_WALK_TO_IDLE +#define CYBER_WALK_TO_IDLE 4 +#endif +#ifndef CYBER_WALK_TO_RANGEATTACK +#define CYBER_WALK_TO_RANGEATTACK 4 +#endif +#ifndef CYBER_WALK_TO_MELEE +#define CYBER_WALK_TO_MELEE 4 +#endif + + class rvmMonsterBossCyberdemon : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterBossCyberdemon ); + + rvmMonsterBossCyberdemon(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextAttack; + float nextNoFOVAttack; + float painNextTime; + idStr range_attack_anim; + idEntityPtr kick_entity; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_boss_guardian.cpp b/neo/doom3/game/ai/AI_monster_boss_guardian.cpp new file mode 100644 index 00000000..418071f9 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_guardian.cpp @@ -0,0 +1,213 @@ +// AI_monster_boss_guardian.cpp + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + +CLASS_DECLARATION( rvmMonsterBase, rvmSeekerBase ) +END_CLASS +CLASS_DECLARATION( rvmMonsterBase, rvmMonsterBossGuardianSpawner ) +END_CLASS +CLASS_DECLARATION( rvmMonsterBase, rvmMonsterBossGuardian ) +END_CLASS + +rvmSeekerBase::rvmSeekerBase() {} +void rvmSeekerBase::Init( idAI* aiOwner ) { rvmMonsterBase::Init( aiOwner ); } +stateResult_t rvmSeekerBase::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "respawnSeeker", respawnSeeker ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} +bool rvmSeekerBase::isAlive() const { return owner && owner->health > 0 && !owner->AI_DEAD; } +stateResult_t rvmSeekerBase::respawnSeeker( stateParms_t* parms ) { return SRESULT_DONE; } + +rvmMonsterBossGuardianSpawner::rvmMonsterBossGuardianSpawner() { guardian = NULL; inactive_time = active_time = pain_time = 0.0f; } +void rvmMonsterBossGuardianSpawner::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + inactive_time = GetFloatKey( "inactive_time", SPAWNER_FADEOUT ); + active_time = GetFloatKey( "active_time", SPAWNER_FADEIN_TIME ); + pain_time = GetFloatKey( "pain_time", SPAWNER_PAIN_FADEOUT ); + SetState( "state_Inactive" ); +} +stateResult_t rvmMonsterBossGuardianSpawner::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "update_effects", update_effects ); + AI_DISPATCH_STATE( "setGuardian", setGuardian ); + AI_DISPATCH_STATE( "state_Inactive", state_Inactive ); + AI_DISPATCH_STATE( "state_Active", state_Active ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} +stateResult_t rvmMonsterBossGuardianSpawner::update_effects( stateParms_t* parms ) { return SRESULT_DONE; } +stateResult_t rvmMonsterBossGuardianSpawner::setGuardian( stateParms_t* parms ) { guardian = scratchEntity.GetEntity(); return SRESULT_DONE; } +stateResult_t rvmMonsterBossGuardianSpawner::state_Inactive( stateParms_t* parms ) { + if ( parms->stage == 0 ) { HideOwner(); IgnoreDamage(); parms->stage = 1; } + if ( Pain() || Activated() ) { ShowOwner(); AllowDamage(); SetState( "state_Active" ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardianSpawner::state_Active( stateParms_t* parms ) { + if ( parms->stage == 0 ) { ShowOwner(); AllowDamage(); scratchEndTime = DelayTime( active_time ); parms->stage = 1; return SRESULT_WAIT; } + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + SetState( "state_Inactive" ); + return SRESULT_DONE; +} +stateResult_t rvmMonsterBossGuardianSpawner::state_Killed( stateParms_t* parms ) { return rvmMonsterBase::state_Killed( parms ); } + +rvmMonsterBossGuardian::rvmMonsterBossGuardian() { + nextAttack = 0.0f; painNextTime = 0.0f; in_melee = false; rightfoot = false; charge = false; + seeker1 = seeker2 = seeker3 = spawner = NULL; +} +void rvmMonsterBossGuardian::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + SetMoveType( MOVETYPE_ANIM ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + SetState( "state_Idle" ); +} +stateResult_t rvmMonsterBossGuardian::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Legs_Sight", Legs_Sight ); + AI_DISPATCH_STATE( "Legs_Death", Legs_Death ); + AI_DISPATCH_STATE( "Legs_Pain", Legs_Pain ); + AI_DISPATCH_STATE( "Legs_RangeAttack", Legs_RangeAttack ); + AI_DISPATCH_STATE( "Legs_MeleeAttack", Legs_MeleeAttack ); + AI_DISPATCH_STATE( "Legs_Walk_Melee_Left", Legs_Walk_Melee_Left ); + AI_DISPATCH_STATE( "Legs_Walk_Melee_Right", Legs_Walk_Melee_Right ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run_Melee_Left", Legs_Run_Melee_Left ); + AI_DISPATCH_STATE( "Legs_Run_Melee_Right", Legs_Run_Melee_Right ); + AI_DISPATCH_STATE( "Legs_Charge", Legs_Charge ); + AI_DISPATCH_STATE( "Legs_Lost", Legs_Lost ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_SpawnSeeker", Legs_SpawnSeeker ); + AI_DISPATCH_STATE( "addSeeker", addSeeker ); + AI_DISPATCH_STATE( "sightEnemy", sightEnemy ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "state_Combat", state_Combat ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_charge", combat_charge ); + AI_DISPATCH_STATE( "pound_impact", pound_impact ); + AI_DISPATCH_STATE( "pound_attack", pound_attack ); + AI_DISPATCH_STATE( "pound_attack_left", pound_attack_left ); + AI_DISPATCH_STATE( "pound_attack_right", pound_attack_right ); + AI_DISPATCH_STATE( "smash_ground", smash_ground ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterBossGuardian::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Pain", GUARDIAN_IDLE_TO_PAIN ); return SRESULT_DONE; } + if ( charge ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Charge", GUARDIAN_IDLE_TO_CHARGE ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", GUARDIAN_IDLE_TO_WALK ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Pain", GUARDIAN_WALK_TO_PAIN ); return SRESULT_DONE; } + if ( charge ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Charge", GUARDIAN_WALK_TO_CHARGE ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", GUARDIAN_WALK_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::Legs_Pain( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, GetPainAnim() ); painNextTime = DelayTime( GUARDIAN_PAIN_DELAY ); parms->stage = 1; return SRESULT_WAIT; } + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Pain", GUARDIAN_PAIN_TO_PAIN ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_LEGS, GUARDIAN_PAIN_TO_IDLE ) ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", GUARDIAN_PAIN_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::Legs_Sight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "sight" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, GUARDIAN_SIGHT_TO_IDLE ) ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", GUARDIAN_SIGHT_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::Legs_Death( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "death" ); parms->stage = 1; } + return AnimDone( ANIMCHANNEL_LEGS, 0 ) ? SRESULT_DONE : SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::Legs_RangeAttack( stateParms_t* parms ) { + if ( parms->stage == 0 ) { StopMove(); FaceEnemy(); PlayAnim( ANIMCHANNEL_LEGS, "range_attack" ); parms->stage = 1; return SRESULT_WAIT; } + if ( AnimDone( ANIMCHANNEL_LEGS, GUARDIAN_RANGEATTACK_TO_IDLE ) ) { FinishAction( "range_attack" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", GUARDIAN_RANGEATTACK_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::Legs_MeleeAttack( stateParms_t* parms ) { + if ( parms->stage == 0 ) { StopMove(); FaceEnemy(); in_melee = true; PlayAnim( ANIMCHANNEL_LEGS, "melee_attack" ); parms->stage = 1; return SRESULT_WAIT; } + if ( AnimDone( ANIMCHANNEL_LEGS, GUARDIAN_MELEE_TO_IDLE ) ) { in_melee = false; FinishAction( "melee_attack" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", GUARDIAN_MELEE_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::Legs_Walk_Melee_Left( stateParms_t* parms ) { if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "walk_melee_left" ); parms->stage = 1; } return AnimDone( ANIMCHANNEL_LEGS, 4 ) ? SRESULT_DONE : SRESULT_WAIT; } +stateResult_t rvmMonsterBossGuardian::Legs_Walk_Melee_Right( stateParms_t* parms ) { if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "walk_melee_right" ); parms->stage = 1; } return AnimDone( ANIMCHANNEL_LEGS, 4 ) ? SRESULT_DONE : SRESULT_WAIT; } +stateResult_t rvmMonsterBossGuardian::Legs_Run_Melee_Left( stateParms_t* parms ) { if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "run_melee_left" ); parms->stage = 1; } return AnimDone( ANIMCHANNEL_LEGS, 4 ) ? SRESULT_DONE : SRESULT_WAIT; } +stateResult_t rvmMonsterBossGuardian::Legs_Run_Melee_Right( stateParms_t* parms ) { if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "run_melee_right" ); parms->stage = 1; } return AnimDone( ANIMCHANNEL_LEGS, 4 ) ? SRESULT_DONE : SRESULT_WAIT; } +stateResult_t rvmMonsterBossGuardian::Legs_Charge( stateParms_t* parms ) { + if ( parms->stage == 0 ) { charge = true; ChargeAttack( "damage_guardian_charge" ); PlayCycle( ANIMCHANNEL_LEGS, "charge" ); parms->stage = 1; return SRESULT_WAIT; } + if ( Pain() ) { charge = false; AttackEnd(); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Pain", GUARDIAN_CHARGE_TO_PAIN ); return SRESULT_DONE; } + if ( MoveDone() || EnemyRange() < 128.0f ) { charge = false; AttackEnd(); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", GUARDIAN_CHARGE_TO_IDLE ); FinishAction( "charge_attack" ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::Legs_Lost( stateParms_t* parms ) { if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "lost" ); parms->stage = 1; } return AnimDone( ANIMCHANNEL_LEGS, GUARDIAN_LOST_TO_IDLE ) ? SRESULT_DONE : SRESULT_WAIT; } +stateResult_t rvmMonsterBossGuardian::Legs_SpawnSeeker( stateParms_t* parms ) { + if ( parms->stage == 0 ) { StopMove(); PlayAnim( ANIMCHANNEL_LEGS, "spawn_seeker" ); parms->stage = 1; return SRESULT_WAIT; } + if ( AnimDone( ANIMCHANNEL_LEGS, GUARDIAN_SPAWN_TO_IDLE ) ) { FinishAction( "spawn_seeker" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", GUARDIAN_SPAWN_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::addSeeker( stateParms_t* parms ) { + idEntity* ent = scratchEntity.GetEntity(); + if ( !seeker1.GetEntity() ) seeker1 = ent; else if ( !seeker2.GetEntity() ) seeker2 = ent; else seeker3 = ent; + return SRESULT_DONE; +} +stateResult_t rvmMonsterBossGuardian::sightEnemy( stateParms_t* parms ) { + FaceEnemy(); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Sight", GUARDIAN_IDLE_TO_SIGHT ); + return SRESULT_DONE; +} +stateResult_t rvmMonsterBossGuardian::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: StartAction( "wait_for_enemy" ); parms->stage = STAGE_WAIT; return SRESULT_WAIT; + case STAGE_WAIT: + if ( InAction() ) return SRESULT_WAIT; + StartAction( "sightEnemy" ); + nextAttack = DelayTime( 2.0f ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} +stateResult_t rvmMonsterBossGuardian::state_Killed( stateParms_t* parms ) { return rvmMonsterBase::state_Killed( parms ); } +stateResult_t rvmMonsterBossGuardian::state_Combat( stateParms_t* parms ) { + enum { STAGE_SELECT, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_SELECT: + if ( EnemyDead() ) { SetState( "state_LostCombat" ); return SRESULT_DONE; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Pain", GUARDIAN_WALK_TO_PAIN ); } + if ( CheckForMelee() ) { StartAction( "combat_melee" ); parms->stage = STAGE_ACTION; return SRESULT_WAIT; } + if ( Time() >= nextAttack && EnemyRange() < GUARDIAN_RANGE_ATTACK_RANGE && CanHitEnemy() ) { StartAction( "combat_range" ); parms->stage = STAGE_ACTION; return SRESULT_WAIT; } + if ( EnemyRange() < GUARDIAN_CHARGE_ATTACK_RANGE && TestChargeAttack() > 0.0f ) { StartAction( "combat_charge" ); parms->stage = STAGE_ACTION; return SRESULT_WAIT; } + MoveToEnemy(); + return SRESULT_WAIT; + case STAGE_ACTION: + if ( InAction() ) { return SRESULT_WAIT; } + parms->stage = STAGE_SELECT; + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} +stateResult_t rvmMonsterBossGuardian::combat_melee( stateParms_t* parms ) { + if ( parms->stage == 0 ) { waitActionName = "melee_attack"; SetAnimState( ANIMCHANNEL_LEGS, "Legs_MeleeAttack", GUARDIAN_IDLE_TO_MELEE ); parms->stage = 1; return SRESULT_WAIT; } + return ActionDone( "melee_attack" ) ? SRESULT_DONE : SRESULT_WAIT; +} +stateResult_t rvmMonsterBossGuardian::combat_range( stateParms_t* parms ) { + if ( parms->stage == 0 ) { waitActionName = "range_attack"; SetAnimState( ANIMCHANNEL_LEGS, "Legs_RangeAttack", GUARDIAN_IDLE_TO_RANGE ); parms->stage = 1; return SRESULT_WAIT; } + if ( !ActionDone( "range_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( GUARDIAN_RANGE_ATTACK_DELAY ); + return SRESULT_DONE; +} +stateResult_t rvmMonsterBossGuardian::combat_charge( stateParms_t* parms ) { + if ( parms->stage == 0 ) { waitActionName = "charge_attack"; charge = true; SetAnimState( ANIMCHANNEL_LEGS, "Legs_Charge", GUARDIAN_IDLE_TO_CHARGE ); parms->stage = 1; return SRESULT_WAIT; } + if ( !ActionDone( "charge_attack" ) ) { return SRESULT_WAIT; } + charge = false; + nextAttack = DelayTime( GUARDIAN_RANGE_ATTACK_DELAY ); + return SRESULT_DONE; +} +stateResult_t rvmMonsterBossGuardian::pound_impact( stateParms_t* parms ) { RadiusDamageFromJoint( rightfoot ? "r_foot" : "l_foot", "damage_guardian_footstep" ); return SRESULT_DONE; } +stateResult_t rvmMonsterBossGuardian::pound_attack( stateParms_t* parms ) { AttackMelee( "damage_guardian_melee" ); return SRESULT_DONE; } +stateResult_t rvmMonsterBossGuardian::pound_attack_left( stateParms_t* parms ) { rightfoot = false; return pound_attack( parms ); } +stateResult_t rvmMonsterBossGuardian::pound_attack_right( stateParms_t* parms ) { rightfoot = true; return pound_attack( parms ); } +stateResult_t rvmMonsterBossGuardian::smash_ground( stateParms_t* parms ) { RadiusDamageFromJoint( "origin", "damage_guardian_smash" ); return SRESULT_DONE; } diff --git a/neo/doom3/game/ai/AI_monster_boss_guardian.h b/neo/doom3/game/ai/AI_monster_boss_guardian.h new file mode 100644 index 00000000..96b2eb03 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_guardian.h @@ -0,0 +1,172 @@ +// AI_monster_boss_guardian.h +#pragma once +#ifndef SPAWNER_PAIN_FADEOUT +#define SPAWNER_PAIN_FADEOUT 2.5f +#endif +#ifndef SPAWNER_FADEOUT +#define SPAWNER_FADEOUT 1.0f +#endif +#ifndef SPAWNER_FADEIN_TIME +#define SPAWNER_FADEIN_TIME 0.25f +#endif +#ifndef GUARDIAN_PAIN_DELAY +#define GUARDIAN_PAIN_DELAY 0.25f +#endif +#ifndef GUARDIAN_RANGE_ATTACK_RANGE +#define GUARDIAN_RANGE_ATTACK_RANGE 1024 +#endif +#ifndef GUARDIAN_RANGE_ATTACK_DELAY +#define GUARDIAN_RANGE_ATTACK_DELAY 7 +#endif +#ifndef GUARDIAN_CHARGE_ATTACK_RANGE +#define GUARDIAN_CHARGE_ATTACK_RANGE 680 +#endif +#ifndef GUARDIAN_NUM_PROJECTILES +#define GUARDIAN_NUM_PROJECTILES 12 +#endif +#ifndef GUARDIAN_PAIN_TO_IDLE +#define GUARDIAN_PAIN_TO_IDLE 3 +#endif +#ifndef GUARDIAN_PAIN_TO_PAIN +#define GUARDIAN_PAIN_TO_PAIN 3 +#endif +#ifndef GUARDIAN_SIGHT_TO_IDLE +#define GUARDIAN_SIGHT_TO_IDLE 8 +#endif +#ifndef GUARDIAN_MELEE_TO_IDLE +#define GUARDIAN_MELEE_TO_IDLE 12 +#endif +#ifndef GUARDIAN_IDLE_TO_PAIN +#define GUARDIAN_IDLE_TO_PAIN 3 +#endif +#ifndef GUARDIAN_IDLE_TO_WALK +#define GUARDIAN_IDLE_TO_WALK 12 +#endif +#ifndef GUARDIAN_IDLE_TO_CHARGE +#define GUARDIAN_IDLE_TO_CHARGE 12 +#endif +#ifndef GUARDIAN_IDLE_TO_MELEE +#define GUARDIAN_IDLE_TO_MELEE 12 +#endif +#ifndef GUARDIAN_IDLE_TO_RANGE +#define GUARDIAN_IDLE_TO_RANGE 12 +#endif +#ifndef GUARDIAN_IDLE_TO_SIGHT +#define GUARDIAN_IDLE_TO_SIGHT 12 +#endif +#ifndef GUARDIAN_IDLE_TO_LOST +#define GUARDIAN_IDLE_TO_LOST 12 +#endif +#ifndef GUARDIAN_WALK_TO_IDLE +#define GUARDIAN_WALK_TO_IDLE 12 +#endif +#ifndef GUARDIAN_WALK_TO_PAIN +#define GUARDIAN_WALK_TO_PAIN 3 +#endif +#ifndef GUARDIAN_WALK_TO_CHARGE +#define GUARDIAN_WALK_TO_CHARGE 8 +#endif +#ifndef GUARDIAN_CHARGE_TO_WALK +#define GUARDIAN_CHARGE_TO_WALK 0 +#endif +#ifndef GUARDIAN_CHARGE_TO_IDLE +#define GUARDIAN_CHARGE_TO_IDLE 12 +#endif +#ifndef GUARDIAN_CHARGE_TO_PAIN +#define GUARDIAN_CHARGE_TO_PAIN 3 +#endif +#ifndef GUARDIAN_LOST_TO_IDLE +#define GUARDIAN_LOST_TO_IDLE 10 +#endif +#ifndef GUARDIAN_LOST_TO_PAIN +#define GUARDIAN_LOST_TO_PAIN 3 +#endif +#ifndef GUARDIAN_LOST_TO_WALK +#define GUARDIAN_LOST_TO_WALK 24 +#endif +#ifndef GUARDIAN_LOST_TO_CHARGE +#define GUARDIAN_LOST_TO_CHARGE 24 +#endif +#ifndef GUARDIAN_RANGEATTACK_TO_IDLE +#define GUARDIAN_RANGEATTACK_TO_IDLE 4 +#endif +#ifndef GUARDIAN_SPAWN_TO_IDLE +#define GUARDIAN_SPAWN_TO_IDLE 4 +#endif +#ifndef GUARDIAN_IDLE_TO_SPAWN +#define GUARDIAN_IDLE_TO_SPAWN 4 +#endif + +class rvmSeekerBase : public rvmMonsterBase { +public: + CLASS_PROTOTYPE( rvmSeekerBase ); + rvmSeekerBase(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + virtual bool isAlive() const; +protected: + virtual stateResult_t respawnSeeker( stateParms_t* parms ); +}; + +class rvmMonsterBossGuardianSpawner : public rvmMonsterBase { +public: + CLASS_PROTOTYPE( rvmMonsterBossGuardianSpawner ); + rvmMonsterBossGuardianSpawner(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; +protected: + idEntityPtr guardian; + float inactive_time; + float active_time; + float pain_time; + virtual stateResult_t update_effects( stateParms_t* parms ); + virtual stateResult_t setGuardian( stateParms_t* parms ); + virtual stateResult_t state_Inactive( stateParms_t* parms ); + virtual stateResult_t state_Active( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); +}; + +class rvmMonsterBossGuardian : public rvmMonsterBase { +public: + CLASS_PROTOTYPE( rvmMonsterBossGuardian ); + rvmMonsterBossGuardian(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; +protected: + float nextAttack; + float painNextTime; + bool in_melee; + bool rightfoot; + bool charge; + idEntityPtr seeker1; + idEntityPtr seeker2; + idEntityPtr seeker3; + idEntityPtr spawner; + virtual stateResult_t Legs_Sight( stateParms_t* parms ); + virtual stateResult_t Legs_Death( stateParms_t* parms ); + virtual stateResult_t Legs_Pain( stateParms_t* parms ); + virtual stateResult_t Legs_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Walk_Melee_Left( stateParms_t* parms ); + virtual stateResult_t Legs_Walk_Melee_Right( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run_Melee_Left( stateParms_t* parms ); + virtual stateResult_t Legs_Run_Melee_Right( stateParms_t* parms ); + virtual stateResult_t Legs_Charge( stateParms_t* parms ); + virtual stateResult_t Legs_Lost( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_SpawnSeeker( stateParms_t* parms ); + virtual stateResult_t addSeeker( stateParms_t* parms ); + virtual stateResult_t sightEnemy( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t state_Combat( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_charge( stateParms_t* parms ); + virtual stateResult_t pound_impact( stateParms_t* parms ); + virtual stateResult_t pound_attack( stateParms_t* parms ); + virtual stateResult_t pound_attack_left( stateParms_t* parms ); + virtual stateResult_t pound_attack_right( stateParms_t* parms ); + virtual stateResult_t smash_ground( stateParms_t* parms ); +}; diff --git a/neo/doom3/game/ai/AI_monster_boss_guardian_seeker.cpp b/neo/doom3/game/ai/AI_monster_boss_guardian_seeker.cpp new file mode 100644 index 00000000..127574c4 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_guardian_seeker.cpp @@ -0,0 +1,252 @@ +// AI_monster_boss_guardian_seeker.cpp + // Native state-machine replacement for ai_monster_boss_guardian_seeker.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmSeekerBase, rvmMonsterBossGuardianSeeker ) + END_CLASS + + rvmMonsterBossGuardianSeeker::rvmMonsterBossGuardianSeeker() { + painNextTime = 0.0f; + nextEnemyCheck = 0.0f; + getCloserEndTime = 0.0f; + retreatEndTime = 0.0f; + guardian = NULL; +} + +void rvmMonsterBossGuardianSeeker::Init( idAI* aiOwner ) { + rvmSeekerBase::Init( aiOwner ); + SetMoveType( MOVETYPE_FLY ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetState( "state_Inactive" ); +} + +stateResult_t rvmMonsterBossGuardianSeeker::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Fly", Torso_Fly ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_Death", Torso_Death ); + AI_DISPATCH_STATE( "see_enemy", see_enemy ); + AI_DISPATCH_STATE( "lost_enemy", lost_enemy ); + AI_DISPATCH_STATE( "respawnSeeker", respawnSeeker ); + AI_DISPATCH_STATE( "state_Respawn", state_Respawn ); + AI_DISPATCH_STATE( "state_Inactive", state_Inactive ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "state_WatchPlayer", state_WatchPlayer ); + AI_DISPATCH_STATE( "state_GetCloser", state_GetCloser ); + AI_DISPATCH_STATE( "state_FindPlayer", state_FindPlayer ); + AI_DISPATCH_STATE( "state_Retreat", state_Retreat ); + AI_DISPATCH_STATE( "state_RunAway", state_RunAway ); + return rvmSeekerBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterBossGuardianSeeker::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Fly", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossGuardianSeeker::Torso_Fly( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "fly" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossGuardianSeeker::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossGuardianSeeker::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossGuardianSeeker::Torso_Death( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "death" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { + FinishAction( "death" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossGuardianSeeker::see_enemy( stateParms_t* parms ) { + LookAtEnemy( SEEKER_SIGHTTIME ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossGuardianSeeker::lost_enemy( stateParms_t* parms ) { + LookAtEnemy( 0.1f ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossGuardianSeeker::respawnSeeker( stateParms_t* parms ) { + SetState( "state_Respawn" ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_Respawn( stateParms_t* parms ) { + enum { STAGE_START, STAGE_SOLID }; + switch ( parms->stage ) { + case STAGE_START: + ShowOwner(); + AllowDamage(); + SetHealth( GetFloatKey( "health", "100" ) ); + StartSoundKey( "snd_respawn", SND_CHANNEL_VOICE, false ); + scratchEndTime = DelayTime( 0.2f ); + parms->stage = STAGE_SOLID; + return SRESULT_WAIT; + case STAGE_SOLID: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + BecomeSolid(); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_Inactive( stateParms_t* parms ) { + if ( parms->stage == 0 ) { HideOwner(); IgnoreDamage(); StopMove(); parms->stage = 1; } + if ( Activated() || Pain() ) { SetState( "state_Respawn" ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: StartAction( "wait_for_enemy" ); parms->stage = STAGE_WAIT; return SRESULT_WAIT; + case STAGE_WAIT: + if ( InAction() ) { return SRESULT_WAIT; } + SetState( "state_WatchPlayer" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_Killed( stateParms_t* parms ) { + if ( parms->stage == 0 ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); BecomeNonSolid(); parms->stage = 1; return SRESULT_WAIT; } + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_Death" ) ) { return SRESULT_WAIT; } + HideOwner(); + SetState( "state_Inactive" ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_WatchPlayer( stateParms_t* parms ) { + const float range = EnemyRange(); + if ( !EnemyVisible() ) { SetState( "state_FindPlayer" ); return SRESULT_DONE; } + if ( CheckForMelee() ) { waitActionName = "melee_attack"; SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 2 ); return SRESULT_WAIT; } + if ( range < SEEKER_TOOCLOSE_RANGE ) { SetState( "state_Retreat" ); return SRESULT_DONE; } + if ( range > SEEKER_TOOFAR_RANGE ) { SetState( "state_GetCloser" ); return SRESULT_DONE; } + FaceEnemy(); + MoveToEnemyHeight(); + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_GetCloser( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MOVE }; + switch ( parms->stage ) { + case STAGE_START: + MoveToEnemy(); + getCloserEndTime = DelayTime( SEEKER_GETCLOSER_TIME ); + parms->stage = STAGE_MOVE; + return SRESULT_WAIT; + case STAGE_MOVE: + if ( Time() >= getCloserEndTime || EnemyRange() <= SEEKER_IDEAL_RANGE || MoveDone() ) { SetState( "state_WatchPlayer" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_FindPlayer( stateParms_t* parms ) { + enum { STAGE_START, STAGE_SEEK }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "lost_enemy" ); + MoveToEnemy(); + scratchEndTime = DelayTime( SEEKER_SIGHTTIME ); + parms->stage = STAGE_SEEK; + return SRESULT_WAIT; + case STAGE_SEEK: + if ( EnemyVisible() ) { StartAction( "see_enemy" ); SetState( "state_WatchPlayer" ); return SRESULT_DONE; } + if ( Time() >= scratchEndTime || MoveDone() ) { SetState( "state_WatchPlayer" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_Retreat( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MOVE }; + switch ( parms->stage ) { + case STAGE_START: + MoveOutOfRange( GetEnemy(), SEEKER_IDEAL_RANGE ); + retreatEndTime = DelayTime( SEEKER_RUNTIME ); + parms->stage = STAGE_MOVE; + return SRESULT_WAIT; + case STAGE_MOVE: + if ( Time() >= retreatEndTime || MoveDone() || EnemyRange() >= SEEKER_IDEAL_RANGE ) { SetState( "state_WatchPlayer" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossGuardianSeeker::state_RunAway( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUN }; + switch ( parms->stage ) { + case STAGE_START: + SetFlySpeed( SEEKER_ESCAPE_SPEED ); + MoveOutOfRange( GetEnemy(), SEEKER_MAXIMUM_RETREAT_RANGE ); + scratchEndTime = DelayTime( SEEKER_RUNTIME ); + parms->stage = STAGE_RUN; + return SRESULT_WAIT; + case STAGE_RUN: + if ( Time() >= scratchEndTime || MoveDone() ) { ClearFlyOffset(); SetState( "state_WatchPlayer" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_boss_guardian_seeker.h b/neo/doom3/game/ai/AI_monster_boss_guardian_seeker.h new file mode 100644 index 00000000..b7dab2d2 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_guardian_seeker.h @@ -0,0 +1,68 @@ +// AI_monster_boss_guardian_seeker.h + // Native state-machine replacement for ai_monster_boss_guardian_seeker.script. + + #pragma once + + + #ifndef SEEKER_TOOCLOSE_RANGE +#define SEEKER_TOOCLOSE_RANGE 200 +#endif +#ifndef SEEKER_TOOFAR_RANGE +#define SEEKER_TOOFAR_RANGE 650 +#endif +#ifndef SEEKER_IDEAL_RANGE +#define SEEKER_IDEAL_RANGE 430 +#endif +#ifndef SEEKER_MAXIMUM_RETREAT_RANGE +#define SEEKER_MAXIMUM_RETREAT_RANGE ( SEEKER_TOOFAR_RANGE + 200 ) +#endif +#ifndef SEEKER_RUNTIME +#define SEEKER_RUNTIME 2 +#endif +#ifndef SEEKER_GETCLOSER_DELAY +#define SEEKER_GETCLOSER_DELAY 3 +#endif +#ifndef SEEKER_GETCLOSER_TIME +#define SEEKER_GETCLOSER_TIME 1 +#endif +#ifndef SEEKER_SIGHTTIME +#define SEEKER_SIGHTTIME 2 +#endif +#ifndef SEEKER_ESCAPE_SPEED +#define SEEKER_ESCAPE_SPEED 800 +#endif + + class rvmMonsterBossGuardianSeeker : public rvmSeekerBase { + public: + CLASS_PROTOTYPE( rvmMonsterBossGuardianSeeker ); + + rvmMonsterBossGuardianSeeker(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float painNextTime; + float nextEnemyCheck; + float getCloserEndTime; + float retreatEndTime; + idEntityPtr guardian; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Fly( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_Death( stateParms_t* parms ); + virtual stateResult_t see_enemy( stateParms_t* parms ); + virtual stateResult_t lost_enemy( stateParms_t* parms ); + virtual stateResult_t respawnSeeker( stateParms_t* parms ); + virtual stateResult_t state_Respawn( stateParms_t* parms ); + virtual stateResult_t state_Inactive( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t state_WatchPlayer( stateParms_t* parms ); + virtual stateResult_t state_GetCloser( stateParms_t* parms ); + virtual stateResult_t state_FindPlayer( stateParms_t* parms ); + virtual stateResult_t state_Retreat( stateParms_t* parms ); + virtual stateResult_t state_RunAway( stateParms_t* parms ); + + }; diff --git a/neo/doom3/game/ai/AI_monster_boss_sabaoth.cpp b/neo/doom3/game/ai/AI_monster_boss_sabaoth.cpp new file mode 100644 index 00000000..828a6121 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_sabaoth.cpp @@ -0,0 +1,308 @@ +// AI_monster_boss_sabaoth.cpp + // Native state-machine replacement for ai_monster_boss_sabaoth.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterBossSabaoth ) + END_CLASS + + rvmMonsterBossSabaoth::rvmMonsterBossSabaoth() { + nextAttack = 0.0f; + nextMeleeAttack = 0.0f; + pathSpeed = 0.0f; + noSlowToStop = false; + currentPath = NULL; + nextPath = NULL; +} + +void rvmMonsterBossSabaoth::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + SetMoveType( MOVETYPE_SLIDE ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterBossSabaoth::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "move_to_corner", move_to_corner ); + AI_DISPATCH_STATE( "turn_to_corner", turn_to_corner ); + AI_DISPATCH_STATE( "closeSection", closeSection ); + AI_DISPATCH_STATE( "openSection", openSection ); + AI_DISPATCH_STATE( "state_Combat", state_Combat ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "fire_bfg", fire_bfg ); + AI_DISPATCH_STATE( "open_section_1", open_section_1 ); + AI_DISPATCH_STATE( "close_section_1", close_section_1 ); + AI_DISPATCH_STATE( "open_section_2", open_section_2 ); + AI_DISPATCH_STATE( "close_section_2", close_section_2 ); + AI_DISPATCH_STATE( "open_section_3", open_section_3 ); + AI_DISPATCH_STATE( "close_section_3", close_section_3 ); + AI_DISPATCH_STATE( "open_section_4", open_section_4 ); + AI_DISPATCH_STATE( "close_section_4", close_section_4 ); + AI_DISPATCH_STATE( "open_section_5", open_section_5 ); + AI_DISPATCH_STATE( "close_section_5", close_section_5 ); + AI_DISPATCH_STATE( "open_section_6", open_section_6 ); + AI_DISPATCH_STATE( "close_section_6", close_section_6 ); + AI_DISPATCH_STATE( "open_section_7", open_section_7 ); + AI_DISPATCH_STATE( "close_section_7", close_section_7 ); + AI_DISPATCH_STATE( "open_section_8", open_section_8 ); + AI_DISPATCH_STATE( "close_section_8", close_section_8 ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterBossSabaoth::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossSabaoth::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + AllowMovement( false ); + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + AllowMovement( true ); + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossSabaoth::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + AllowMovement( false ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + AllowMovement( true ); + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossSabaoth::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "idle" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossSabaoth::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossSabaoth::state_Killed( stateParms_t* parms ) { + return rvmMonsterBase::state_Killed( parms ); +} + +stateResult_t rvmMonsterBossSabaoth::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_SLIDE ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossSabaoth::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = DelayTime( 1.0f ); + nextMeleeAttack = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossSabaoth::move_to_corner( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + if ( scratchEntity.GetEntity() ) { MoveToEntity( scratchEntity.GetEntity() ); } else { MoveToEnemy(); } + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( check_attacks( false ) != 0 ) { return SRESULT_DONE; } + if ( !MoveDone() ) { return SRESULT_WAIT; } + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossSabaoth::turn_to_corner( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + if ( scratchEntity.GetEntity() ) { FaceEntity( scratchEntity.GetEntity() ); } else { FaceEnemy(); } + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( check_attacks( false ) != 0 ) { return SRESULT_DONE; } + if ( !FacingIdeal() ) { return SRESULT_WAIT; } + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossSabaoth::closeSection( stateParms_t* parms ) { + // Native hook for the map section-close target dispatch used by the script. + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossSabaoth::openSection( stateParms_t* parms ) { + // Native hook for the map section-open target dispatch used by the script. + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossSabaoth::state_Combat( stateParms_t* parms ) { + enum { STAGE_SELECT, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_SELECT: + if ( EnemyDead() ) { SetState( "state_LostCombat" ); return SRESULT_DONE; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); } + if ( do_attack( check_attacks( false ) ) ) { parms->stage = STAGE_ACTION; return SRESULT_WAIT; } + if ( !MoveDone() ) { return SRESULT_WAIT; } + MoveToEnemy(); + return SRESULT_WAIT; + case STAGE_ACTION: + if ( InAction() ) { return SRESULT_WAIT; } + parms->stage = STAGE_SELECT; + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +int rvmMonsterBossSabaoth::check_attacks( bool meleeOnly ) { + int flags = 0; + if ( CheckForMelee() && Time() >= nextMeleeAttack ) { flags |= ATTACK_MELEE; } + if ( !meleeOnly && Time() >= nextAttack && CanHitEnemyFromAnim( "range_attack" ) ) { flags |= ATTACK_MISSILE; } + return static_cast( flags ); +} + +int rvmMonsterBossSabaoth::check_attacks() { + return check_attacks( false ); +} + +bool rvmMonsterBossSabaoth::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterBossSabaoth::combat_range( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "range_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "range_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( SABAOTH_ATTACK_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossSabaoth::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + nextMeleeAttack = DelayTime( SABAOTH_ATTACK_RATE_MELEE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossSabaoth::fire_bfg( stateParms_t* parms ) { + AttackMissile( "bfg" ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossSabaoth::set_section_open( stateParms_t* parms, int section, bool open ) { + // Hook point for firing section-specific map targets. section is 1-8, open selects open/close. + scratchCount = section; + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossSabaoth::open_section_1( stateParms_t* parms ) { return set_section_open( parms, 1, true ); } +stateResult_t rvmMonsterBossSabaoth::close_section_1( stateParms_t* parms ) { return set_section_open( parms, 1, false ); } + +stateResult_t rvmMonsterBossSabaoth::open_section_2( stateParms_t* parms ) { return set_section_open( parms, 2, true ); } +stateResult_t rvmMonsterBossSabaoth::close_section_2( stateParms_t* parms ) { return set_section_open( parms, 2, false ); } + +stateResult_t rvmMonsterBossSabaoth::open_section_3( stateParms_t* parms ) { return set_section_open( parms, 3, true ); } +stateResult_t rvmMonsterBossSabaoth::close_section_3( stateParms_t* parms ) { return set_section_open( parms, 3, false ); } + +stateResult_t rvmMonsterBossSabaoth::open_section_4( stateParms_t* parms ) { return set_section_open( parms, 4, true ); } +stateResult_t rvmMonsterBossSabaoth::close_section_4( stateParms_t* parms ) { return set_section_open( parms, 4, false ); } + +stateResult_t rvmMonsterBossSabaoth::open_section_5( stateParms_t* parms ) { return set_section_open( parms, 5, true ); } +stateResult_t rvmMonsterBossSabaoth::close_section_5( stateParms_t* parms ) { return set_section_open( parms, 5, false ); } + +stateResult_t rvmMonsterBossSabaoth::open_section_6( stateParms_t* parms ) { return set_section_open( parms, 6, true ); } +stateResult_t rvmMonsterBossSabaoth::close_section_6( stateParms_t* parms ) { return set_section_open( parms, 6, false ); } + +stateResult_t rvmMonsterBossSabaoth::open_section_7( stateParms_t* parms ) { return set_section_open( parms, 7, true ); } +stateResult_t rvmMonsterBossSabaoth::close_section_7( stateParms_t* parms ) { return set_section_open( parms, 7, false ); } + +stateResult_t rvmMonsterBossSabaoth::open_section_8( stateParms_t* parms ) { return set_section_open( parms, 8, true ); } +stateResult_t rvmMonsterBossSabaoth::close_section_8( stateParms_t* parms ) { return set_section_open( parms, 8, false ); } diff --git a/neo/doom3/game/ai/AI_monster_boss_sabaoth.h b/neo/doom3/game/ai/AI_monster_boss_sabaoth.h new file mode 100644 index 00000000..6f964267 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_sabaoth.h @@ -0,0 +1,79 @@ +// AI_monster_boss_sabaoth.h + // Native state-machine replacement for ai_monster_boss_sabaoth.script. + + #pragma once + + + #ifndef SABAOTH_ATTACK_RATE +#define SABAOTH_ATTACK_RATE 4 +#endif +#ifndef SABAOTH_ATTACK_RATE_MELEE +#define SABAOTH_ATTACK_RATE_MELEE 1 +#endif +#ifndef SABAOTH_SLOW_DIST +#define SABAOTH_SLOW_DIST 64 +#endif +#ifndef SECTION_MOVE_DIST +#define SECTION_MOVE_DIST 190 +#endif +#ifndef CENTER_DIST +#define CENTER_DIST 304 +#endif +#ifndef MELEE_INIT +#define MELEE_INIT 290 +#endif + + class rvmMonsterBossSabaoth : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterBossSabaoth ); + + rvmMonsterBossSabaoth(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextAttack; + float nextMeleeAttack; + float pathSpeed; + bool noSlowToStop; + idEntityPtr currentPath; + idEntityPtr nextPath; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t move_to_corner( stateParms_t* parms ); + virtual stateResult_t turn_to_corner( stateParms_t* parms ); + virtual stateResult_t closeSection( stateParms_t* parms ); + virtual stateResult_t openSection( stateParms_t* parms ); + virtual stateResult_t state_Combat( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t fire_bfg( stateParms_t* parms ); + virtual stateResult_t open_section_1( stateParms_t* parms ); + virtual stateResult_t close_section_1( stateParms_t* parms ); + virtual stateResult_t open_section_2( stateParms_t* parms ); + virtual stateResult_t close_section_2( stateParms_t* parms ); + virtual stateResult_t open_section_3( stateParms_t* parms ); + virtual stateResult_t close_section_3( stateParms_t* parms ); + virtual stateResult_t open_section_4( stateParms_t* parms ); + virtual stateResult_t close_section_4( stateParms_t* parms ); + virtual stateResult_t open_section_5( stateParms_t* parms ); + virtual stateResult_t close_section_5( stateParms_t* parms ); + virtual stateResult_t open_section_6( stateParms_t* parms ); + virtual stateResult_t close_section_6( stateParms_t* parms ); + virtual stateResult_t open_section_7( stateParms_t* parms ); + virtual stateResult_t close_section_7( stateParms_t* parms ); + virtual stateResult_t open_section_8( stateParms_t* parms ); + virtual stateResult_t close_section_8( stateParms_t* parms ); + virtual int check_attacks( bool meleeOnly ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + stateResult_t set_section_open( stateParms_t* parms, int section, bool open ); + + }; diff --git a/neo/doom3/game/ai/AI_monster_boss_vagary.cpp b/neo/doom3/game/ai/AI_monster_boss_vagary.cpp new file mode 100644 index 00000000..e61fe128 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_vagary.cpp @@ -0,0 +1,323 @@ +// AI_monster_boss_vagary.cpp + // Native state-machine replacement for ai_monster_boss_vagary.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterBossVagary ) + END_CLASS + + rvmMonsterBossVagary::rvmMonsterBossVagary() { + nextDodge = 0.0f; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + painNextTime = 0.0f; + combat_node = NULL; + throwEntity = NULL; +} + +void rvmMonsterBossVagary::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterBossVagary::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "throw_object", throw_object ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterBossVagary::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", VAGARY_IDLE_TO_PAIN ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossVagary::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( VAGARY_PAIN_DELAY ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", VAGARY_PAIN_TO_PAIN ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, VAGARY_PAIN_TO_IDLE ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", VAGARY_PAIN_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossVagary::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, VAGARY_MELEE_TO_IDLE ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", VAGARY_MELEE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossVagary::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 1.0f ); + if ( AnimDone( ANIMCHANNEL_TORSO, VAGARY_RANGE_TO_IDLE ) ) { + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", VAGARY_RANGE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossVagary::Torso_TurretAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, VAGARY_TURRET_TO_IDLE ) ) { + AllowMovement( true ); + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossVagary::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "stand" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", VAGARY_IDLE_TO_WALK ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossVagary::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", VAGARY_WALK_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossVagary::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, VAGARY_DODGE_LEFT_TO_IDLE ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", VAGARY_DODGE_LEFT_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossVagary::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, VAGARY_DODGE_RIGHT_TO_IDLE ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", VAGARY_DODGE_RIGHT_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterBossVagary::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossVagary::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextDodge = RandomTime( VAGARY_DODGE_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterBossVagary::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + if ( !CanReachEnemy() || currentTime >= nextAttack ) { + if ( CanHitEnemy() ) { + throwEntity = FindActorsInBounds( idVec3( -512, -512, -512 ), idVec3( 512, 512, 512 ) ); + if ( throwEntity.GetEntity() ) { flags |= ATTACK_MISSILE; } + } + } + } + return static_cast( flags ); +} + +bool rvmMonsterBossVagary::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + VAGARY_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterBossVagary::throw_object( stateParms_t* parms ) { + idEntity* ent = throwEntity.GetEntity(); + if ( !ent ) { return SRESULT_DONE; } + // Hook point for the project-specific vagary_ThrowObjectAtEnemy helper. + StartSoundKey( "snd_pickup", SND_CHANNEL_WEAPON, false ); + if ( GetEnemy() ) { + idVec3 dir = GetEnemyPos() - ent->GetPhysics()->GetOrigin(); + dir.Normalize(); + ent->GetPhysics()->SetLinearVelocity( dir * VAGARY_THROW_SPEED2 ); + } + return SRESULT_DONE; +} + +stateResult_t rvmMonsterBossVagary::combat_range( stateParms_t* parms ) { + enum { STAGE_FACE, STAGE_THROW, STAGE_COOLDOWN }; + switch ( parms->stage ) { + case STAGE_FACE: + FaceEnemy(); + LookAtEntity( throwEntity.GetEntity(), 3.0f ); + scratchEndTime = DelayTime( 0.3f ); + parms->stage = STAGE_THROW; + return SRESULT_WAIT; + case STAGE_THROW: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + StartAction( "throw_object" ); + scratchEndTime = DelayTime( 1.0f ); + parms->stage = STAGE_COOLDOWN; + return SRESULT_WAIT; + case STAGE_COOLDOWN: + if ( InAction() || Time() < scratchEndTime ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + nextAttack = DelayTime( VAGARY_ATTACK_RATE ); + nextNoFOVAttack = Time() + VAGARY_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossVagary::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", VAGARY_WALK_TO_MELEE ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossVagary::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", VAGARY_WALK_TO_DODGE_LEFT ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( VAGARY_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterBossVagary::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", VAGARY_WALK_TO_DODGE_RIGHT ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( VAGARY_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_boss_vagary.h b/neo/doom3/game/ai/AI_monster_boss_vagary.h new file mode 100644 index 00000000..291a7c38 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_boss_vagary.h @@ -0,0 +1,109 @@ +// AI_monster_boss_vagary.h + // Native state-machine replacement for ai_monster_boss_vagary.script. + + #pragma once + + + #ifndef VAGARY_ATTACK_RATE +#define VAGARY_ATTACK_RATE 1.5f +#endif +#ifndef VAGARY_DODGE_RATE +#define VAGARY_DODGE_RATE 4 +#endif +#ifndef VAGARY_PAIN_DELAY +#define VAGARY_PAIN_DELAY 0.25f +#endif +#ifndef VAGARY_TURRET_TO_IDLE +#define VAGARY_TURRET_TO_IDLE 4 +#endif +#ifndef VAGARY_THROW_SPEED +#define VAGARY_THROW_SPEED 1200 +#endif +#ifndef VAGARY_THROW_SPEED2 +#define VAGARY_THROW_SPEED2 1500 +#endif +#ifndef VAGARY_THROW_DIST +#define VAGARY_THROW_DIST 200 +#endif +#ifndef VAGARY_THROW_OFFSET +#define VAGARY_THROW_OFFSET 64 +#endif +#ifndef VAGARY_NOFOVTIME +#define VAGARY_NOFOVTIME 4 +#endif +#ifndef VAGARY_PAIN_TO_IDLE +#define VAGARY_PAIN_TO_IDLE 2 +#endif +#ifndef VAGARY_PAIN_TO_PAIN +#define VAGARY_PAIN_TO_PAIN 0 +#endif +#ifndef VAGARY_MELEE_TO_IDLE +#define VAGARY_MELEE_TO_IDLE 4 +#endif +#ifndef VAGARY_RANGE_TO_IDLE +#define VAGARY_RANGE_TO_IDLE 4 +#endif +#ifndef VAGARY_DODGE_LEFT_TO_IDLE +#define VAGARY_DODGE_LEFT_TO_IDLE 4 +#endif +#ifndef VAGARY_DODGE_RIGHT_TO_IDLE +#define VAGARY_DODGE_RIGHT_TO_IDLE 4 +#endif +#ifndef VAGARY_IDLE_TO_PAIN +#define VAGARY_IDLE_TO_PAIN 0 +#endif +#ifndef VAGARY_IDLE_TO_WALK +#define VAGARY_IDLE_TO_WALK 4 +#endif +#ifndef VAGARY_WALK_TO_IDLE +#define VAGARY_WALK_TO_IDLE 4 +#endif +#ifndef VAGARY_WALK_TO_RANGEATTACK +#define VAGARY_WALK_TO_RANGEATTACK 4 +#endif +#ifndef VAGARY_WALK_TO_MELEE +#define VAGARY_WALK_TO_MELEE 4 +#endif +#ifndef VAGARY_WALK_TO_DODGE_LEFT +#define VAGARY_WALK_TO_DODGE_LEFT 4 +#endif +#ifndef VAGARY_WALK_TO_DODGE_RIGHT +#define VAGARY_WALK_TO_DODGE_RIGHT 4 +#endif + + class rvmMonsterBossVagary : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterBossVagary ); + + rvmMonsterBossVagary(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextDodge; + float nextAttack; + float nextNoFOVAttack; + float painNextTime; + idEntityPtr combat_node; + idEntityPtr throwEntity; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t throw_object( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_archvile.cpp b/neo/doom3/game/ai/AI_monster_demon_archvile.cpp new file mode 100644 index 00000000..e10ab54d --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_archvile.cpp @@ -0,0 +1,465 @@ +// AI_monster_demon_archvile.cpp + // Native state-machine replacement for ai_monster_demon_archvile.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonArchvile ) + END_CLASS + + rvmMonsterDemonArchvile::rvmMonsterDemonArchvile() { + nextDodge = 0.0f; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + painNextTime = 0.0f; + keep_alive = 0.0f; + num_start = 0.0f; + summon_delay = 0.0f; + summon_time = 0.0f; + combat_node = NULL; + resurrectionTarget = NULL; + flameWallOrigin = vec3_zero; + incinerateOrigin = vec3_zero; +} + +void rvmMonsterDemonArchvile::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + keep_alive = GetFloatKey( "keep_alive", "0" ); + num_start = GetFloatKey( "num_start", "0" ); + summon_delay = GetFloatKey( "summon_delay", "0" ); + summon_time = Time() + summon_delay; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonArchvile::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_Resurrect", Torso_Resurrect ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_FlameWallAttack", Torso_FlameWallAttack ); + AI_DISPATCH_STATE( "Torso_IncinerateAttack", Torso_IncinerateAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_resurrect", combat_resurrect ); + AI_DISPATCH_STATE( "combat_flamewall", combat_flamewall ); + AI_DISPATCH_STATE( "combat_incinerate", combat_incinerate ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + AI_DISPATCH_STATE( "spawn_flame_wall", spawn_flame_wall ); + AI_DISPATCH_STATE( "spawn_incerate", spawn_incerate ); + AI_DISPATCH_STATE( "flame_wall_loop", flame_wall_loop ); + AI_DISPATCH_STATE( "flame_wall", flame_wall ); + AI_DISPATCH_STATE( "incinerate", incinerate ); + AI_DISPATCH_STATE( "resurrection", resurrection ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonArchvile::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", ARCH_IDLE_TO_PAIN ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonArchvile::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( ARCH_PAIN_DELAY ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", ARCH_PAIN_TO_PAIN ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, ARCH_PAIN_TO_IDLE ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", ARCH_PAIN_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::Torso_Resurrect( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + SetSmokeVisibility( -1, true ); + PlayAnim( ANIMCHANNEL_TORSO, "resurrection" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, ARCH_RESURRECT_TO_IDLE ) ) { + SetSmokeVisibility( -1, false ); + FinishAction( "resurrect" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", ARCH_RESURRECT_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 0.2f ); + if ( AnimDone( ANIMCHANNEL_TORSO, ARCH_MELEE_TO_IDLE ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", ARCH_MELEE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::Torso_FlameWallAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + SetSmokeVisibility( -1, true ); + PlayAnim( ANIMCHANNEL_TORSO, "flamewall_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 1.0f ); + if ( AnimDone( ANIMCHANNEL_TORSO, ARCH_FLAMEWALL_TO_IDLE ) ) { + SetSmokeVisibility( -1, false ); + nextAttack = DelayTime( ARCH_ATTACK_RATE ); + FinishAction( "flamewall_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", ARCH_FLAMEWALL_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::Torso_IncinerateAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "incinerate_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 1.0f ); + if ( AnimDone( ANIMCHANNEL_TORSO, ARCH_INCINERATE_TO_IDLE ) ) { + nextAttack = DelayTime( ARCH_ATTACK_RATE ); + FinishAction( "incinerate_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", ARCH_INCINERATE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::Torso_TurretAttack( stateParms_t* parms ) { + // The DoomScript turret attack can resurrect, flame-wall, or incinerate from the same entry. + // Choose once at entry, then delegate to the concrete non-blocking torso state. + if ( parms->stage == 0 ) { + if ( num_minions() < keep_alive && Time() >= summon_time ) { + waitActionName = "resurrect"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Resurrect", ARCH_IDLE_TO_TURRETATTACK ); + } else if ( EnemyRange() < ARCH_FLAME_WALL_RANGE && CanHitEnemy() ) { + waitActionName = "flamewall_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_FlameWallAttack", ARCH_IDLE_TO_TURRETATTACK ); + } else { + waitActionName = "incinerate_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_IncinerateAttack", ARCH_IDLE_TO_TURRETATTACK ); + } + parms->stage = 1; + return SRESULT_WAIT; + } + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_TurretAttack" ) ) { return SRESULT_WAIT; } + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", ARCH_TURRETATTACK_TO_IDLE ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterDemonArchvile::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "stand" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", ARCH_IDLE_TO_WALK ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonArchvile::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", ARCH_WALK_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonArchvile::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, ARCH_DODGE_LEFT_TO_IDLE ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", ARCH_DODGE_LEFT_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonArchvile::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, ARCH_DODGE_RIGHT_TO_IDLE ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", ARCH_DODGE_RIGHT_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonArchvile::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextDodge = RandomTime( ARCH_DODGE_RATE ); + summon_time = Time() + summon_delay; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +float rvmMonsterDemonArchvile::num_minions() { + // Integration hook: the script counted live minion targets from its target list. Return zero here + // so a native port can opt into resurrect behavior without blocking; replace with target iteration + // if your project exposes the original target array to native AI objects. + return 0; +} + +int rvmMonsterDemonArchvile::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( num_minions() < keep_alive && currentTime >= summon_time ) { flags |= ATTACK_RESURRECT; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + if ( currentTime >= nextAttack ) { + if ( EnemyRange() < ARCH_FLAME_WALL_RANGE && CanHitEnemy() ) { flags |= ATTACK_FLAMEWALL; } + if ( CanHitEnemy() ) { flags |= ATTACK_MISSILE; } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonArchvile::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + ARCH_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_RESURRECT ) { StartAction( "combat_resurrect" ); return true; } + if ( flags & ATTACK_FLAMEWALL ) { StartAction( "combat_flamewall" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_incinerate" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonArchvile::combat_resurrect( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "resurrect"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Resurrect", ARCH_IDLE_TO_RESURRECT ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "resurrect" ) ) { return SRESULT_WAIT; } + summon_time = Time() + summon_delay; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::combat_flamewall( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "flamewall_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_FlameWallAttack", ARCH_IDLE_TO_FLAMEWALLATTACK ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "flamewall_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( ARCH_ATTACK_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::combat_incinerate( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "incinerate_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_IncinerateAttack", ARCH_IDLE_TO_INCINERATEATTACK ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "incinerate_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( ARCH_ATTACK_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", ARCH_WALK_TO_MELEE ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", ARCH_WALK_TO_DODGE_LEFT ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( ARCH_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", ARCH_WALK_TO_DODGE_RIGHT ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( ARCH_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::spawn_flame_wall( stateParms_t* parms ) { + // Hook point for the original script's repeated sys.spawn of flame-wall projectiles. + // The looping behavior is represented by flame_wall_loop() below, not a blocking wait. + return SRESULT_DONE; +} + +stateResult_t rvmMonsterDemonArchvile::spawn_incerate( stateParms_t* parms ) { + // Hook point for the original script's incinerate projectile spawn. + return SRESULT_DONE; +} + +stateResult_t rvmMonsterDemonArchvile::flame_wall_loop( stateParms_t* parms ) { + enum { STAGE_INIT, STAGE_SPAWN, STAGE_DELAY }; + switch ( parms->stage ) { + case STAGE_INIT: + scratchCount = 0; + parms->stage = STAGE_SPAWN; + return SRESULT_WAIT; + case STAGE_SPAWN: + if ( scratchCount >= ARCH_FLAME_WALL_NUM ) { return SRESULT_DONE; } + flameWallOrigin = GetEnemyPos(); + StartAction( "spawn_flame_wall" ); + scratchCount++; + scratchEndTime = DelayTime( 0.05f ); + parms->stage = STAGE_DELAY; + return SRESULT_WAIT; + case STAGE_DELAY: + if ( InAction() || Time() < scratchEndTime ) { return SRESULT_WAIT; } + parms->stage = STAGE_SPAWN; + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonArchvile::flame_wall( stateParms_t* parms ) { + if ( parms->stage == 0 ) { StartAction( "flame_wall_loop" ); parms->stage = 1; return SRESULT_WAIT; } + return InAction() ? SRESULT_WAIT : SRESULT_DONE; +} + +stateResult_t rvmMonsterDemonArchvile::incinerate( stateParms_t* parms ) { + incinerateOrigin = GetEnemyPos(); + StartAction( "spawn_incerate" ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterDemonArchvile::resurrection( stateParms_t* parms ) { + // Hook point for resurrecting a selected dead minion. This remains non-blocking. + resurrectionTarget = NULL; + return SRESULT_DONE; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_archvile.h b/neo/doom3/game/ai/AI_monster_demon_archvile.h new file mode 100644 index 00000000..da191f9c --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_archvile.h @@ -0,0 +1,173 @@ +// AI_monster_demon_archvile.h + // Native state-machine replacement for ai_monster_demon_archvile.script. + + #pragma once + + + #ifndef ARCH_ATTACK_RATE +#define ARCH_ATTACK_RATE 2 +#endif +#ifndef ARCH_DODGE_RATE +#define ARCH_DODGE_RATE 4 +#endif +#ifndef ARCH_PAIN_DELAY +#define ARCH_PAIN_DELAY 0.25f +#endif +#ifndef ARCH_TURRET_TO_IDLE +#define ARCH_TURRET_TO_IDLE 4 +#endif +#ifndef ARCH_FLAME_WALL_NUM +#define ARCH_FLAME_WALL_NUM 20 +#endif +#ifndef ARCH_FLAME_WALL_SEPERATION +#define ARCH_FLAME_WALL_SEPERATION 48 +#endif +#ifndef ARCH_FLAME_WALL_RANGE +#define ARCH_FLAME_WALL_RANGE ( ARCH_FLAME_WALL_NUM * ARCH_FLAME_WALL_SEPERATION ) +#endif +#ifndef ARCH_NOFOVTIME +#define ARCH_NOFOVTIME 4 +#endif +#ifndef ARCH_PAIN_TO_IDLE +#define ARCH_PAIN_TO_IDLE 2 +#endif +#ifndef ARCH_PAIN_TO_PAIN +#define ARCH_PAIN_TO_PAIN 0 +#endif +#ifndef ARCH_SIGHT_TO_IDLE +#define ARCH_SIGHT_TO_IDLE 4 +#endif +#ifndef ARCH_RESURRECT_TO_IDLE +#define ARCH_RESURRECT_TO_IDLE 4 +#endif +#ifndef ARCH_IDLE_TO_RESURRECT +#define ARCH_IDLE_TO_RESURRECT 4 +#endif +#ifndef ARCH_MELEE_TO_IDLE +#define ARCH_MELEE_TO_IDLE 4 +#endif +#ifndef ARCH_INCINERATE_TO_IDLE +#define ARCH_INCINERATE_TO_IDLE 4 +#endif +#ifndef ARCH_FLAMEWALL_TO_IDLE +#define ARCH_FLAMEWALL_TO_IDLE 4 +#endif +#ifndef ARCH_DODGE_LEFT_TO_IDLE +#define ARCH_DODGE_LEFT_TO_IDLE 4 +#endif +#ifndef ARCH_DODGE_RIGHT_TO_IDLE +#define ARCH_DODGE_RIGHT_TO_IDLE 4 +#endif +#ifndef ARCH_WAIT_TO_OUT +#define ARCH_WAIT_TO_OUT 4 +#endif +#ifndef ARCH_IN_TO_WAIT +#define ARCH_IN_TO_WAIT 4 +#endif +#ifndef ARCH_WALK_TO_WAIT +#define ARCH_WALK_TO_WAIT 4 +#endif +#ifndef ARCH_WALK_TO_ACTION +#define ARCH_WALK_TO_ACTION 4 +#endif +#ifndef ARCH_ACTION_TO_IDLE +#define ARCH_ACTION_TO_IDLE 4 +#endif +#ifndef ARCH_WALK_TO_DODGE_LEFT +#define ARCH_WALK_TO_DODGE_LEFT 4 +#endif +#ifndef ARCH_WALK_TO_DODGE_RIGHT +#define ARCH_WALK_TO_DODGE_RIGHT 4 +#endif +#ifndef ARCH_IDLE_TO_PAIN +#define ARCH_IDLE_TO_PAIN 0 +#endif +#ifndef ARCH_IDLE_TO_WALK +#define ARCH_IDLE_TO_WALK 4 +#endif +#ifndef ARCH_IDLE_TO_SIGHT +#define ARCH_IDLE_TO_SIGHT 4 +#endif +#ifndef ARCH_WALK_TO_IDLE +#define ARCH_WALK_TO_IDLE 4 +#endif +#ifndef ARCH_WALK_TO_FLAMEWALLATTACK +#define ARCH_WALK_TO_FLAMEWALLATTACK 4 +#endif +#ifndef ARCH_IDLE_TO_FLAMEWALLATTACK +#define ARCH_IDLE_TO_FLAMEWALLATTACK 4 +#endif +#ifndef ARCH_WALK_TO_INCINERATEATTACK +#define ARCH_WALK_TO_INCINERATEATTACK 4 +#endif +#ifndef ARCH_IDLE_TO_INCINERATEATTACK +#define ARCH_IDLE_TO_INCINERATEATTACK 4 +#endif +#ifndef ARCH_WALK_TO_MELEE +#define ARCH_WALK_TO_MELEE 4 +#endif +#ifndef ARCH_IDLE_TO_TURRETATTACK +#define ARCH_IDLE_TO_TURRETATTACK 4 +#endif +#ifndef ARCH_TURRETATTACK_TO_IDLE +#define ARCH_TURRETATTACK_TO_IDLE 4 +#endif +#ifndef ATTACK_FLAMEWALL +#define ATTACK_FLAMEWALL ATTACK_SPECIAL1 +#endif +#ifndef ATTACK_RESURRECT +#define ATTACK_RESURRECT ATTACK_SPECIAL2 +#endif + + class rvmMonsterDemonArchvile : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonArchvile ); + + rvmMonsterDemonArchvile(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextDodge; + float nextAttack; + float nextNoFOVAttack; + float painNextTime; + float keep_alive; + float num_start; + float summon_delay; + float summon_time; + idEntityPtr combat_node; + idEntityPtr resurrectionTarget; + idVec3 flameWallOrigin; + idVec3 incinerateOrigin; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_Resurrect( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_FlameWallAttack( stateParms_t* parms ); + virtual stateResult_t Torso_IncinerateAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_resurrect( stateParms_t* parms ); + virtual stateResult_t combat_flamewall( stateParms_t* parms ); + virtual stateResult_t combat_incinerate( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual stateResult_t spawn_flame_wall( stateParms_t* parms ); + virtual stateResult_t spawn_incerate( stateParms_t* parms ); + virtual stateResult_t flame_wall_loop( stateParms_t* parms ); + virtual stateResult_t flame_wall( stateParms_t* parms ); + virtual stateResult_t incinerate( stateParms_t* parms ); + virtual stateResult_t resurrection( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + virtual float num_minions(); + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_cherub.cpp b/neo/doom3/game/ai/AI_monster_demon_cherub.cpp new file mode 100644 index 00000000..e69c6f50 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_cherub.cpp @@ -0,0 +1,343 @@ +// AI_monster_demon_cherub.cpp + // Native state-machine replacement for ai_monster_demon_cherub.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonCherub ) + END_CLASS + + rvmMonsterDemonCherub::rvmMonsterDemonCherub() { + explode = false; + nextDodge = 0.0f; + nextLeap = 0.0f; + painNextTime = 0.0f; + jumpVelocity = vec3_zero; + combat_node = NULL; +} + +void rvmMonsterDemonCherub::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + run_distance = CHERUB_RUNDISTANCE; + walk_turn = CHERUB_WALKTURN; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonCherub::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_LeapAttack", Torso_LeapAttack ); + AI_DISPATCH_STATE( "Torso_Walk", Torso_Walk ); + AI_DISPATCH_STATE( "Torso_Run", Torso_Run ); + AI_DISPATCH_STATE( "Torso_DodgeLeft", Torso_DodgeLeft ); + AI_DISPATCH_STATE( "Torso_DodgeRight", Torso_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_leap", combat_leap ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + AI_DISPATCH_STATE( "path_jump", path_jump ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonCherub::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Run", 4 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonCherub::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonCherub::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonCherub::Torso_LeapAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_START_WAIT, STAGE_LOOP, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + OverrideAnim( ANIMCHANNEL_LEGS ); + PreventPain( 1.0f ); + PlayAnim( ANIMCHANNEL_TORSO, "jump_start" ); + parms->stage = STAGE_START_WAIT; + return SRESULT_WAIT; + case STAGE_START_WAIT: + if ( !AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { return SRESULT_WAIT; } + AttackBegin( "melee_cherubLeapAttack" ); + SetLinearVelocity( jumpVelocity ); + PlayCycle( ANIMCHANNEL_TORSO, "jump_loop" ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( owner && !owner->AI_ONGROUND ) { return SRESULT_WAIT; } + AttackEnd(); + PlayAnim( ANIMCHANNEL_TORSO, "jump_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "leap_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonCherub::Torso_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "walk" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Run", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonCherub::Torso_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "run" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Walk", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonCherub::Torso_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_TORSO, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonCherub::Torso_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_TORSO, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonCherub::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonCherub::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextLeap = RandomTime( CHERUB_LEAP_RATE ); + nextDodge = RandomTime( CHERUB_DODGE_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonCherub::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( EnemyInFOV() ) { + const float range = EnemyRange(); + if ( range >= 0 && range < CHERUB_LEAP_RANGE && currentTime >= nextLeap && CanHitEnemy() ) { + const float t = AnimLength( ANIMCHANNEL_TORSO, "jump_start" ); + const idVec3 jumpTarget = PredictEnemyPos( t ); + jumpVelocity = GetJumpVelocity( jumpTarget, CHERUB_LEAP_SPEED, CHERUB_LEAP_MAXHEIGHT ); + if ( jumpVelocity.LengthSqr() > 0.001f ) { flags |= ATTACK_LEAP; } else { nextLeap = DelayTime( 2.0f ); } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonCherub::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_LEAP ) { StartAction( "combat_leap" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonCherub::combat_leap( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + TurnToPos( GetOrigin() + jumpVelocity ); + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + nextLeap = DelayTime( CHERUB_LEAP_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonCherub::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonCherub::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_DodgeLeft", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( CHERUB_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonCherub::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_DodgeRight", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( CHERUB_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonCherub::path_jump( stateParms_t* parms ) { + enum { STAGE_PATH_CORNER, STAGE_SETUP, STAGE_TURN, STAGE_LEAP, STAGE_DONE }; + idEntity* path = current_path.GetEntity(); + switch ( parms->stage ) { + case STAGE_PATH_CORNER: + StartAction( "path_corner" ); + parms->stage = STAGE_SETUP; + return SRESULT_WAIT; + case STAGE_SETUP: { + if ( InAction() ) { return SRESULT_WAIT; } + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + idEntity* target = RandomPathFrom( path ); + if ( !target ) { return SRESULT_DONE; } + scratchEntity = target; + const idVec3 jumpTarget = target->GetPhysics()->GetOrigin(); + if ( path && path->spawnArgs.GetString( "up", "" )[0] ) { + float forward = path->spawnArgs.GetFloat( "forward", "0" ); + idVec3 dir = jumpTarget - GetOrigin(); + dir.z = 0.0f; + dir.Normalize(); + jumpVelocity = dir * forward; + jumpVelocity.z = path->spawnArgs.GetFloat( "up", "0" ); + } else { + jumpVelocity = GetJumpVelocity( jumpTarget, CHERUB_LEAP_SPEED, 1024.0f ); + } + StopMove(); + TurnToPos( jumpTarget ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + } + case STAGE_TURN: + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + if ( !FacingIdeal() ) { return SRESULT_WAIT; } + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_LEAP; + return SRESULT_WAIT; + case STAGE_LEAP: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + StopMove(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_cherub.h b/neo/doom3/game/ai/AI_monster_demon_cherub.h new file mode 100644 index 00000000..e5ebb28a --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_cherub.h @@ -0,0 +1,63 @@ +// AI_monster_demon_cherub.h + // Native state-machine replacement for ai_monster_demon_cherub.script. + + #pragma once + + + #ifndef CHERUB_RUNDISTANCE +#define CHERUB_RUNDISTANCE 192 +#endif +#ifndef CHERUB_WALKTURN +#define CHERUB_WALKTURN 65 +#endif +#ifndef CHERUB_LEAP_RATE +#define CHERUB_LEAP_RATE 2 +#endif +#ifndef CHERUB_DODGE_RATE +#define CHERUB_DODGE_RATE 2 +#endif +#ifndef CHERUB_LEAP_RANGE +#define CHERUB_LEAP_RANGE 1024 +#endif +#ifndef CHERUB_LEAP_SPEED +#define CHERUB_LEAP_SPEED 650 +#endif +#ifndef CHERUB_LEAP_MAXHEIGHT +#define CHERUB_LEAP_MAXHEIGHT 48 +#endif + + class rvmMonsterDemonCherub : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonCherub ); + + rvmMonsterDemonCherub(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + bool explode; + float nextDodge; + float nextLeap; + float painNextTime; + idVec3 jumpVelocity; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_LeapAttack( stateParms_t* parms ); + virtual stateResult_t Torso_Walk( stateParms_t* parms ); + virtual stateResult_t Torso_Run( stateParms_t* parms ); + virtual stateResult_t Torso_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Torso_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_leap( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual stateResult_t path_jump( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_hellknight.cpp b/neo/doom3/game/ai/AI_monster_demon_hellknight.cpp new file mode 100644 index 00000000..9cd273c2 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_hellknight.cpp @@ -0,0 +1,246 @@ +// AI_monster_demon_hellknight.cpp + // Native state-machine replacement for ai_monster_demon_hellknight.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonHellknight ) + END_CLASS + + rvmMonsterDemonHellknight::rvmMonsterDemonHellknight() { + nextDodge = 0.0f; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextLeap = 0.0f; + painNextTime = 0.0f; + jumpVelocity = vec3_zero; + combat_node = NULL; +} + +void rvmMonsterDemonHellknight::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonHellknight::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonHellknight::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonHellknight::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", HELLKNIGHT_PAIN_TO_PAIN ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, HELLKNIGHT_PAIN_TO_IDLE ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", HELLKNIGHT_PAIN_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonHellknight::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, HELLKNIGHT_MELEE_TO_IDLE ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", HELLKNIGHT_MELEE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonHellknight::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, range_attack_anim.c_str() ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 1.0f ); + if ( AnimDone( ANIMCHANNEL_TORSO, HELLKNIGHT_RANGE_TO_IDLE ) ) { + AllowMovement( true ); + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", HELLKNIGHT_RANGE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonHellknight::Torso_TurretAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "turret_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + FaceEnemy(); + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + AllowMovement( true ); + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonHellknight::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "stand" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonHellknight::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonHellknight::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonHellknight::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextDodge = 0.0f; + nextLeap = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonHellknight::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + if ( !CanReachEnemy() || currentTime >= nextAttack ) { + range_attack_anim = ChooseAnim( ANIMCHANNEL_LEGS, "turret_attack" ); + if ( CanHitEnemyFromAnim( range_attack_anim.c_str() ) ) { flags |= ATTACK_MISSILE; } + const char* movingAnim = ChooseAnim( ANIMCHANNEL_LEGS, "range_attack" ); + if ( TestAnimMoveTowardEnemy( movingAnim ) && CanHitEnemyFromAnim( movingAnim ) ) { + range_attack_anim = movingAnim; + flags |= ATTACK_MISSILE; + } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonHellknight::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + HELLKNIGHT_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonHellknight::combat_range( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + waitActionName = "range_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "range_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( HELLKNIGHT_ATTACK_RATE ); + nextNoFOVAttack = Time() + HELLKNIGHT_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonHellknight::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_hellknight.h b/neo/doom3/game/ai/AI_monster_demon_hellknight.h new file mode 100644 index 00000000..50218643 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_hellknight.h @@ -0,0 +1,61 @@ +// AI_monster_demon_hellknight.h + // Native state-machine replacement for ai_monster_demon_hellknight.script. + + #pragma once + + + #ifndef HELLKNIGHT_ATTACK_RATE +#define HELLKNIGHT_ATTACK_RATE 3 +#endif +#ifndef HELLKNIGHT_PAIN_DELAY +#define HELLKNIGHT_PAIN_DELAY 0.25f +#endif +#ifndef HELLKNIGHT_NOFOVTIME +#define HELLKNIGHT_NOFOVTIME 6 +#endif +#ifndef HELLKNIGHT_PAIN_TO_IDLE +#define HELLKNIGHT_PAIN_TO_IDLE 2 +#endif +#ifndef HELLKNIGHT_PAIN_TO_PAIN +#define HELLKNIGHT_PAIN_TO_PAIN 0 +#endif +#ifndef HELLKNIGHT_MELEE_TO_IDLE +#define HELLKNIGHT_MELEE_TO_IDLE 4 +#endif +#ifndef HELLKNIGHT_RANGE_TO_IDLE +#define HELLKNIGHT_RANGE_TO_IDLE 4 +#endif + + class rvmMonsterDemonHellknight : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonHellknight ); + + rvmMonsterDemonHellknight(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextDodge; + float nextAttack; + float nextNoFOVAttack; + float nextLeap; + float painNextTime; + idVec3 jumpVelocity; + idStr range_attack_anim; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_imp.cpp b/neo/doom3/game/ai/AI_monster_demon_imp.cpp new file mode 100644 index 00000000..0bec920f --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_imp.cpp @@ -0,0 +1,435 @@ +// AI_monster_demon_imp.cpp + // Native state-machine replacement for ai_monster_demon_imp.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonImp ) + END_CLASS + + rvmMonsterDemonImp::rvmMonsterDemonImp() { + nextDodge = 0.0f; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextLeap = 0.0f; + painNextTime = 0.0f; + jumpVelocity = vec3_zero; + combat_node = NULL; +} + +void rvmMonsterDemonImp::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + run_distance = IMP_RUNDISTANCE; + walk_turn = IMP_WALKTURN; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonImp::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttackIdle", Torso_TurretAttackIdle ); + AI_DISPATCH_STATE( "Torso_LeapAttack", Torso_LeapAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_leap", combat_leap ); + AI_DISPATCH_STATE( "path_jump", path_jump ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonImp::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonImp::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 6 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 6 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, range_attack_anim.c_str() ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 1.0f ); + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + AllowMovement( true ); + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::Torso_TurretAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "turret_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + FaceEnemy(); + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + AllowMovement( true ); + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::Torso_TurretAttackIdle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { FaceEnemy(); IdleAnim( ANIMCHANNEL_TORSO, "turret_idle" ); parms->stage = 1; } + FaceEnemy(); + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonImp::Torso_LeapAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_START_WAIT, STAGE_LOOP, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + OverrideAnim( ANIMCHANNEL_LEGS ); + PreventPain( 1.0f ); + PlayAnim( ANIMCHANNEL_TORSO, "jump_start" ); + parms->stage = STAGE_START_WAIT; + return SRESULT_WAIT; + case STAGE_START_WAIT: + if ( !AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { return SRESULT_WAIT; } + AttackBegin( "melee_impLeapAttack" ); + SetLinearVelocity( jumpVelocity ); + PlayCycle( ANIMCHANNEL_TORSO, "jump_loop" ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( owner && !owner->AI_ONGROUND ) { return SRESULT_WAIT; } + AttackEnd(); + PlayAnim( ANIMCHANNEL_TORSO, "jump_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "leap_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "stand" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonImp::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonImp::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonImp::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonImp::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonImp::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextDodge = RandomTime( IMP_DODGE_RATE ); + nextLeap = RandomTime( IMP_LEAP_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonImp::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + const float range = EnemyRange(); + if ( range >= IMP_LEAP_RANGE_MIN && range < IMP_LEAP_RANGE_MAX && currentTime >= nextLeap && CanHitEnemy() ) { + const float t = AnimLength( ANIMCHANNEL_TORSO, "jump_start" ); + const idVec3 jumpTarget = PredictEnemyPos( t ); + jumpVelocity = GetJumpVelocity( jumpTarget, IMP_LEAP_SPEED, IMP_LEAP_MAXHEIGHT ); + if ( jumpVelocity.LengthSqr() > 0.001f ) { flags |= ATTACK_LEAP; } else { nextLeap = DelayTime( 2.0f ); } + } + if ( !CanReachEnemy() || currentTime >= nextAttack ) { + range_attack_anim = ChooseAnim( ANIMCHANNEL_LEGS, "turret_attack" ); + if ( CanHitEnemyFromAnim( range_attack_anim.c_str() ) ) { flags |= ATTACK_MISSILE; } + const char* movingAnim = ChooseAnim( ANIMCHANNEL_LEGS, "range_attack" ); + if ( TestAnimMoveTowardEnemy( movingAnim ) && CanHitEnemyFromAnim( movingAnim ) ) { + range_attack_anim = movingAnim; + flags |= ATTACK_MISSILE; + } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonImp::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + IMP_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_LEAP ) { StartAction( "combat_leap" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonImp::combat_range( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + waitActionName = "range_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "range_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( IMP_ATTACK_RATE ); + nextNoFOVAttack = Time() + IMP_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::combat_leap( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + TurnToPos( GetOrigin() + jumpVelocity ); + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + nextLeap = DelayTime( IMP_LEAP_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::path_jump( stateParms_t* parms ) { + enum { STAGE_PATH_CORNER, STAGE_SETUP, STAGE_TURN, STAGE_LEAP, STAGE_DONE }; + idEntity* path = current_path.GetEntity(); + switch ( parms->stage ) { + case STAGE_PATH_CORNER: + StartAction( "path_corner" ); + parms->stage = STAGE_SETUP; + return SRESULT_WAIT; + case STAGE_SETUP: { + if ( InAction() ) { return SRESULT_WAIT; } + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + idEntity* target = RandomPathFrom( path ); + if ( !target ) { return SRESULT_DONE; } + scratchEntity = target; + const idVec3 jumpTarget = target->GetPhysics()->GetOrigin(); + if ( path && path->spawnArgs.GetString( "up", "" )[0] ) { + float forward = path->spawnArgs.GetFloat( "forward", "0" ); + idVec3 dir = jumpTarget - GetOrigin(); + dir.z = 0.0f; + dir.Normalize(); + jumpVelocity = dir * forward; + jumpVelocity.z = path->spawnArgs.GetFloat( "up", "0" ); + } else { + jumpVelocity = GetJumpVelocity( jumpTarget, IMP_LEAP_SPEED, 1024.0f ); + } + StopMove(); + TurnToPos( jumpTarget ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + } + case STAGE_TURN: + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + if ( !FacingIdeal() ) { return SRESULT_WAIT; } + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_LEAP; + return SRESULT_WAIT; + case STAGE_LEAP: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + StopMove(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + StopMove(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( IMP_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonImp::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + StopMove(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( IMP_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_imp.h b/neo/doom3/game/ai/AI_monster_demon_imp.h new file mode 100644 index 00000000..61595425 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_imp.h @@ -0,0 +1,79 @@ +// AI_monster_demon_imp.h + // Native state-machine replacement for ai_monster_demon_imp.script. + + #pragma once + + + #ifndef IMP_RUNDISTANCE +#define IMP_RUNDISTANCE 192 +#endif +#ifndef IMP_WALKTURN +#define IMP_WALKTURN 65 +#endif +#ifndef IMP_ATTACK_RATE +#define IMP_ATTACK_RATE 1 +#endif +#ifndef IMP_LEAP_RATE +#define IMP_LEAP_RATE 7 +#endif +#ifndef IMP_LEAP_SPEED +#define IMP_LEAP_SPEED 650 +#endif +#ifndef IMP_LEAP_MAXHEIGHT +#define IMP_LEAP_MAXHEIGHT 48 +#endif +#ifndef IMP_DODGE_RATE +#define IMP_DODGE_RATE 8 +#endif +#ifndef IMP_LEAP_RANGE_MIN +#define IMP_LEAP_RANGE_MIN 200 +#endif +#ifndef IMP_LEAP_RANGE_MAX +#define IMP_LEAP_RANGE_MAX 480 +#endif +#ifndef IMP_NOFOVTIME +#define IMP_NOFOVTIME 4 +#endif + + class rvmMonsterDemonImp : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonImp ); + + rvmMonsterDemonImp(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextDodge; + float nextAttack; + float nextNoFOVAttack; + float nextLeap; + float painNextTime; + idVec3 jumpVelocity; + idStr range_attack_anim; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttackIdle( stateParms_t* parms ); + virtual stateResult_t Torso_LeapAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_leap( stateParms_t* parms ); + virtual stateResult_t path_jump( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_maggot.cpp b/neo/doom3/game/ai/AI_monster_demon_maggot.cpp new file mode 100644 index 00000000..bdfe5fcc --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_maggot.cpp @@ -0,0 +1,349 @@ +// AI_monster_demon_maggot.cpp + // Native state-machine replacement for ai_monster_demon_maggot.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonMaggot ) + END_CLASS + + rvmMonsterDemonMaggot::rvmMonsterDemonMaggot() { + explode = false; + nextDodge = 0.0f; + nextLeap = 0.0f; + painNextTime = 0.0f; + jumpVelocity = vec3_zero; + combat_node = NULL; +} + +void rvmMonsterDemonMaggot::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + run_distance = MAGGOT_RUNDISTANCE; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonMaggot::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_LeapAttack", Torso_LeapAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_leap", combat_leap ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + AI_DISPATCH_STATE( "path_jump", path_jump ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonMaggot::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMaggot::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMaggot::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMaggot::Torso_LeapAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_START_WAIT, STAGE_LOOP, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + OverrideAnim( ANIMCHANNEL_LEGS ); + PreventPain( 1.0f ); + PlayAnim( ANIMCHANNEL_TORSO, "jump_start" ); + parms->stage = STAGE_START_WAIT; + return SRESULT_WAIT; + case STAGE_START_WAIT: + if ( !AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { return SRESULT_WAIT; } + AttackBegin( "melee_maggotLeapAttack" ); + SetLinearVelocity( jumpVelocity ); + PlayCycle( ANIMCHANNEL_TORSO, "jump_loop" ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( owner && !owner->AI_ONGROUND ) { return SRESULT_WAIT; } + AttackEnd(); + PlayAnim( ANIMCHANNEL_TORSO, "jump_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "leap_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMaggot::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "idle" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMaggot::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMaggot::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMaggot::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMaggot::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMaggot::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMaggot::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextLeap = RandomTime( MAGGOT_LEAP_RATE ); + nextDodge = RandomTime( MAGGOT_DODGE_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonMaggot::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( EnemyInFOV() ) { + const float range = EnemyRange(); + if ( range >= MAGGOT_LEAP_RANGE_MIN && range < MAGGOT_LEAP_RANGE_MAX && currentTime >= nextLeap && CanHitEnemy() ) { + const float t = AnimLength( ANIMCHANNEL_TORSO, "jump_start" ); + const idVec3 jumpTarget = PredictEnemyPos( t ); + jumpVelocity = GetJumpVelocity( jumpTarget, MAGGOT_LEAP_SPEED, MAGGOT_LEAP_MAXHEIGHT ); + if ( jumpVelocity.LengthSqr() > 0.001f ) { flags |= ATTACK_LEAP; } else { nextLeap = DelayTime( 2.0f ); } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonMaggot::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_LEAP ) { StartAction( "combat_leap" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonMaggot::combat_leap( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + TurnToPos( GetOrigin() + jumpVelocity ); + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + nextLeap = DelayTime( MAGGOT_LEAP_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMaggot::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMaggot::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + MoveToEnemy(); + nextDodge = DelayTime( MAGGOT_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMaggot::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + MoveToEnemy(); + nextDodge = DelayTime( MAGGOT_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMaggot::path_jump( stateParms_t* parms ) { + enum { STAGE_PATH_CORNER, STAGE_SETUP, STAGE_TURN, STAGE_LEAP, STAGE_DONE }; + idEntity* path = current_path.GetEntity(); + switch ( parms->stage ) { + case STAGE_PATH_CORNER: + StartAction( "path_corner" ); + parms->stage = STAGE_SETUP; + return SRESULT_WAIT; + case STAGE_SETUP: { + if ( InAction() ) { return SRESULT_WAIT; } + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + idEntity* target = RandomPathFrom( path ); + if ( !target ) { return SRESULT_DONE; } + scratchEntity = target; + const idVec3 jumpTarget = target->GetPhysics()->GetOrigin(); + if ( path && path->spawnArgs.GetString( "up", "" )[0] ) { + float forward = path->spawnArgs.GetFloat( "forward", "0" ); + idVec3 dir = jumpTarget - GetOrigin(); + dir.z = 0.0f; + dir.Normalize(); + jumpVelocity = dir * forward; + jumpVelocity.z = path->spawnArgs.GetFloat( "up", "0" ); + } else { + jumpVelocity = GetJumpVelocity( jumpTarget, MAGGOT_LEAP_SPEED, 1024.0f ); + } + StopMove(); + TurnToPos( jumpTarget ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + } + case STAGE_TURN: + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + if ( !FacingIdeal() ) { return SRESULT_WAIT; } + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_LEAP; + return SRESULT_WAIT; + case STAGE_LEAP: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + StopMove(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_maggot.h b/neo/doom3/game/ai/AI_monster_demon_maggot.h new file mode 100644 index 00000000..442bf66f --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_maggot.h @@ -0,0 +1,67 @@ +// AI_monster_demon_maggot.h + // Native state-machine replacement for ai_monster_demon_maggot.script. + + #pragma once + + + #ifndef MAGGOT_RUNDISTANCE +#define MAGGOT_RUNDISTANCE 192 +#endif +#ifndef MAGGOT_ATTACK_RATE +#define MAGGOT_ATTACK_RATE 1 +#endif +#ifndef MAGGOT_LEAP_RATE +#define MAGGOT_LEAP_RATE 7 +#endif +#ifndef MAGGOT_LEAP_SPEED +#define MAGGOT_LEAP_SPEED 650 +#endif +#ifndef MAGGOT_LEAP_MAXHEIGHT +#define MAGGOT_LEAP_MAXHEIGHT 48 +#endif +#ifndef MAGGOT_DODGE_RATE +#define MAGGOT_DODGE_RATE 8 +#endif +#ifndef MAGGOT_LEAP_RANGE_MIN +#define MAGGOT_LEAP_RANGE_MIN 200 +#endif +#ifndef MAGGOT_LEAP_RANGE_MAX +#define MAGGOT_LEAP_RANGE_MAX 480 +#endif + + class rvmMonsterDemonMaggot : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonMaggot ); + + rvmMonsterDemonMaggot(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + bool explode; + float nextDodge; + float nextLeap; + float painNextTime; + idVec3 jumpVelocity; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_LeapAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_leap( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual stateResult_t path_jump( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_mancubus.cpp b/neo/doom3/game/ai/AI_monster_demon_mancubus.cpp new file mode 100644 index 00000000..d13c1439 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_mancubus.cpp @@ -0,0 +1,247 @@ +// AI_monster_demon_mancubus.cpp + // Native state-machine replacement for ai_monster_demon_mancubus.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonMancubus ) + END_CLASS + + rvmMonsterDemonMancubus::rvmMonsterDemonMancubus() { + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + fire = 0.0f; + painNextTime = 0.0f; + combat_node = NULL; +} + +void rvmMonsterDemonMancubus::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + walk_turn = MANC_WALKTURN; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonMancubus::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonMancubus::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( fire > 0.0f ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMancubus::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMancubus::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMancubus::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_START_WAIT, STAGE_LOOP_START, STAGE_LOOP_WAIT, STAGE_END, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_start" ); + parms->stage = STAGE_START_WAIT; + return SRESULT_WAIT; + case STAGE_START_WAIT: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { return SRESULT_WAIT; } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_LOOP_START: + if ( fire <= 0.0f ) { parms->stage = STAGE_END; return SRESULT_WAIT; } + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_loop" ); + parms->stage = STAGE_LOOP_WAIT; + return SRESULT_WAIT; + case STAGE_LOOP_WAIT: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { return SRESULT_WAIT; } + fire -= 1.0f; + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_END: + fire = 0.0f; + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { SetBlendFrames( ANIMCHANNEL_TORSO, 4 ); } + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { FinishAction( "range_attack" ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMancubus::Torso_TurretAttack( stateParms_t* parms ) { + // Uses the same staged burst sequence as Torso_RangeAttack, but completes the turret action token. + stateResult_t r = Torso_RangeAttack( parms ); + if ( r == SRESULT_DONE ) { FinishAction( "turret_attack" ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); } + return r; +} + +stateResult_t rvmMonsterDemonMancubus::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "stand" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 2 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMancubus::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 2 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonMancubus::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMancubus::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonMancubus::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + if ( !canMelee ) { combat_node = GetCombatNode(); if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + if ( !CanReachEnemy() || Time() >= nextAttack ) { + if ( CanHitEnemyFromAnim( "range_attack_loop" ) ) { flags |= ATTACK_MISSILE; } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonMancubus::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + MANC_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonMancubus::combat_range( stateParms_t* parms ) { + enum { STAGE_START, STAGE_DELAY, STAGE_FIRE, STAGE_FINISH }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + scratchEndTime = EnemyInFOV() ? Time() : DelayTime( 0.4f ); + parms->stage = STAGE_DELAY; + return SRESULT_WAIT; + case STAGE_DELAY: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + fire = MANC_MIN_SHOTS + idMath::Ftoi( RandomFloat( MANC_MAX_SHOTS - MANC_MIN_SHOTS ) ); + waitActionName = "range_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); + parms->stage = STAGE_FIRE; + return SRESULT_WAIT; + case STAGE_FIRE: + if ( fire > 0.0f ) { + if ( !CanHitEnemyFromAnim( "range_attack_loop" ) ) { fire = 0.0f; } + if ( Pain() && ( check_attacks() & ATTACK_MELEE ) ) { fire = 0.0f; } + return SRESULT_WAIT; + } + parms->stage = STAGE_FINISH; + return SRESULT_WAIT; + case STAGE_FINISH: + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( MANC_ATTACK_RATE ); + nextNoFOVAttack = Time() + MANC_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonMancubus::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_mancubus.h b/neo/doom3/game/ai/AI_monster_demon_mancubus.h new file mode 100644 index 00000000..aa9ffeee --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_mancubus.h @@ -0,0 +1,52 @@ +// AI_monster_demon_mancubus.h + // Native state-machine replacement for ai_monster_demon_mancubus.script. + + #pragma once + + + #ifndef MANC_WALKTURN +#define MANC_WALKTURN 65 +#endif +#ifndef MANC_ATTACK_RATE +#define MANC_ATTACK_RATE 1 +#endif +#ifndef MANC_MAX_SHOTS +#define MANC_MAX_SHOTS 5 +#endif +#ifndef MANC_MIN_SHOTS +#define MANC_MIN_SHOTS 1 +#endif +#ifndef MANC_NOFOVTIME +#define MANC_NOFOVTIME 8 +#endif + + class rvmMonsterDemonMancubus : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonMancubus ); + + rvmMonsterDemonMancubus(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextAttack; + float nextNoFOVAttack; + float fire; + float painNextTime; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_pinky.cpp b/neo/doom3/game/ai/AI_monster_demon_pinky.cpp new file mode 100644 index 00000000..350be364 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_pinky.cpp @@ -0,0 +1,274 @@ +// AI_monster_demon_pinky.cpp + // Native state-machine replacement for ai_monster_demon_pinky.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonPinky ) + END_CLASS + + rvmMonsterDemonPinky::rvmMonsterDemonPinky() { + nextDodge = 0.0f; + nextRoar = 0.0f; + painNextTime = 0.0f; + combat_node = NULL; +} + +void rvmMonsterDemonPinky::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonPinky::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_Roar", Torso_Roar ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_roar", combat_roar ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonPinky::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "stand" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonPinky::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonPinky::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonPinky::Torso_Roar( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + PlayAnim( ANIMCHANNEL_TORSO, "roar" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "roar" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonPinky::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "idle" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonPinky::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonPinky::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonPinky::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonPinky::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonPinky::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonPinky::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextRoar = RandomTime( PINKY_ROAR_RATE ); + nextDodge = RandomTime( PINKY_DODGE_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonPinky::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( EnemyInFOV() && currentTime >= nextRoar ) { flags |= ATTACK_MISSILE; } + return static_cast( flags ); +} + +bool rvmMonsterDemonPinky::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_roar" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonPinky::combat_roar( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + StopMove(); + waitActionName = "roar"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Roar", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "roar" ) ) { return SRESULT_WAIT; } + nextRoar = DelayTime( PINKY_ROAR_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonPinky::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonPinky::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( PINKY_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonPinky::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( PINKY_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_pinky.h b/neo/doom3/game/ai/AI_monster_demon_pinky.h new file mode 100644 index 00000000..be16a499 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_pinky.h @@ -0,0 +1,46 @@ +// AI_monster_demon_pinky.h + // Native state-machine replacement for ai_monster_demon_pinky.script. + + #pragma once + + + #ifndef PINKY_ROAR_RATE +#define PINKY_ROAR_RATE 5 +#endif +#ifndef PINKY_DODGE_RATE +#define PINKY_DODGE_RATE 5 +#endif + + class rvmMonsterDemonPinky : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonPinky ); + + rvmMonsterDemonPinky(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextDodge; + float nextRoar; + float painNextTime; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_Roar( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_roar( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_revenant.cpp b/neo/doom3/game/ai/AI_monster_demon_revenant.cpp new file mode 100644 index 00000000..553fa9ee --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_revenant.cpp @@ -0,0 +1,310 @@ +// AI_monster_demon_revenant.cpp + // Native state-machine replacement for ai_monster_demon_revenant.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonRevenant ) + END_CLASS + + rvmMonsterDemonRevenant::rvmMonsterDemonRevenant() { + nextDodge = 0.0f; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextLeap = 0.0f; + painNextTime = 0.0f; + jumpVelocity = vec3_zero; + combat_node = NULL; +} + +void rvmMonsterDemonRevenant::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + walk_turn = REV_WALKTURN; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonRevenant::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonRevenant::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonRevenant::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 7 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 7 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 7 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonRevenant::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonRevenant::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, range_attack_anim.c_str() ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 1.0f ); + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + AllowMovement( true ); + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonRevenant::Torso_TurretAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "turret_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + FaceEnemy(); + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + AllowMovement( true ); + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonRevenant::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "idle" ); parms->stage = 1; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonRevenant::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonRevenant::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonRevenant::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonRevenant::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonRevenant::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextDodge = RandomTime( REV_DODGE_RATE ); + nextLeap = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonRevenant::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + if ( !CanReachEnemy() || currentTime >= nextAttack ) { + range_attack_anim = ChooseAnim( ANIMCHANNEL_LEGS, "turret_attack" ); + if ( CanHitEnemyFromAnim( range_attack_anim.c_str() ) ) { flags |= ATTACK_MISSILE; } + const char* movingAnim = ChooseAnim( ANIMCHANNEL_LEGS, "range_attack" ); + if ( TestAnimMoveTowardEnemy( movingAnim ) && CanHitEnemyFromAnim( movingAnim ) ) { + range_attack_anim = movingAnim; + flags |= ATTACK_MISSILE; + } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonRevenant::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + REV_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonRevenant::combat_range( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + waitActionName = "range_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "range_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( REV_ATTACK_RATE ); + nextNoFOVAttack = Time() + REV_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonRevenant::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonRevenant::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + StopMove(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( REV_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonRevenant::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + StopMove(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( REV_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_revenant.h b/neo/doom3/game/ai/AI_monster_demon_revenant.h new file mode 100644 index 00000000..690e7316 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_revenant.h @@ -0,0 +1,56 @@ +// AI_monster_demon_revenant.h + // Native state-machine replacement for ai_monster_demon_revenant.script. + + #pragma once + + + #ifndef REV_WALKTURN +#define REV_WALKTURN 65 +#endif +#ifndef REV_ATTACK_RATE +#define REV_ATTACK_RATE 1.2f +#endif +#ifndef REV_DODGE_RATE +#define REV_DODGE_RATE 8 +#endif +#ifndef REV_NOFOVTIME +#define REV_NOFOVTIME 4 +#endif + + class rvmMonsterDemonRevenant : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonRevenant ); + + rvmMonsterDemonRevenant(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextDodge; + float nextAttack; + float nextNoFOVAttack; + float nextLeap; + float painNextTime; + idVec3 jumpVelocity; + idStr range_attack_anim; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_trite.cpp b/neo/doom3/game/ai/AI_monster_demon_trite.cpp new file mode 100644 index 00000000..98273672 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_trite.cpp @@ -0,0 +1,441 @@ +// AI_monster_demon_trite.cpp + // Native state-machine replacement for ai_monster_demon_trite.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonTrite ) + END_CLASS + + rvmMonsterDemonTrite::rvmMonsterDemonTrite() { + explode = false; + nextDodge = 0.0f; + nextLeap = 0.0f; + painNextTime = 0.0f; + jumpVelocity = vec3_zero; + combat_node = NULL; +} + +void rvmMonsterDemonTrite::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + run_distance = TRITE_RUNDISTANCE; + walk_turn = TRITE_WALKTURN; + explode = GetIntKey( "no_explode" ) == 0; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonTrite::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_LeapAttack", Torso_LeapAttack ); + AI_DISPATCH_STATE( "Torso_Walk", Torso_Walk ); + AI_DISPATCH_STATE( "Torso_Run", Torso_Run ); + AI_DISPATCH_STATE( "Torso_DodgeLeft", Torso_DodgeLeft ); + AI_DISPATCH_STATE( "Torso_DodgeRight", Torso_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_leap", combat_leap ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + AI_DISPATCH_STATE( "path_jump", path_jump ); + AI_DISPATCH_STATE( "state_LostCombat", state_LostCombat ); + AI_DISPATCH_STATE( "state_Explode", state_Explode ); + AI_DISPATCH_STATE( "combat_explode", combat_explode ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonTrite::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Run", 4 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonTrite::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::Torso_LeapAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_START_WAIT, STAGE_LOOP, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + OverrideAnim( ANIMCHANNEL_LEGS ); + PreventPain( 1.0f ); + PlayAnim( ANIMCHANNEL_TORSO, "jump_start" ); + parms->stage = STAGE_START_WAIT; + return SRESULT_WAIT; + case STAGE_START_WAIT: + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { return SRESULT_WAIT; } + FinishAction( "leap_attack_start" ); + AttackBegin( "melee_triteLeapAttack" ); + SetLinearVelocity( jumpVelocity ); + PlayCycle( ANIMCHANNEL_TORSO, "jump_loop" ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( owner && !owner->AI_ONGROUND ) { return SRESULT_WAIT; } + AttackEnd(); + PlayAnim( ANIMCHANNEL_TORSO, "jump_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "leap_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::Torso_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "walk" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Run", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonTrite::Torso_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "run" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Walk", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonTrite::Torso_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_TORSO, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonTrite::Torso_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_TORSO, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonTrite::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextLeap = RandomTime( TRITE_LEAP_RATE ); + nextDodge = RandomTime( TRITE_DODGE_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonTrite::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( EnemyInFOV() ) { + const float range = EnemyRange(); + if ( range >= TRITE_LEAP_RANGE_MIN && range < TRITE_LEAP_RANGE_MAX && currentTime >= nextLeap && CanHitEnemy() ) { + const float t = AnimLength( ANIMCHANNEL_TORSO, "jump_start" ); + const idVec3 jumpTarget = PredictEnemyPos( t ); + jumpVelocity = GetJumpVelocity( jumpTarget, TRITE_LEAP_SPEED, TRITE_LEAP_MAXHEIGHT ); + if ( jumpVelocity.LengthSqr() > 0.001f ) { flags |= ATTACK_LEAP; } else { nextLeap = DelayTime( 2.0f ); } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonTrite::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_LEAP ) { StartAction( "combat_leap" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonTrite::combat_leap( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + TurnToPos( GetOrigin() + jumpVelocity ); + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + nextLeap = DelayTime( TRITE_LEAP_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_DodgeLeft", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( TRITE_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_DodgeRight", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( TRITE_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::path_jump( stateParms_t* parms ) { + enum { STAGE_PATH_CORNER, STAGE_SETUP, STAGE_TURN, STAGE_LEAP, STAGE_DONE }; + idEntity* path = current_path.GetEntity(); + switch ( parms->stage ) { + case STAGE_PATH_CORNER: + StartAction( "path_corner" ); + parms->stage = STAGE_SETUP; + return SRESULT_WAIT; + case STAGE_SETUP: { + if ( InAction() ) { return SRESULT_WAIT; } + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + idEntity* target = RandomPathFrom( path ); + if ( !target ) { return SRESULT_DONE; } + scratchEntity = target; + const idVec3 jumpTarget = target->GetPhysics()->GetOrigin(); + if ( path && path->spawnArgs.GetString( "up", "" )[0] ) { + float forward = path->spawnArgs.GetFloat( "forward", "0" ); + idVec3 dir = jumpTarget - GetOrigin(); + dir.z = 0.0f; + dir.Normalize(); + jumpVelocity = dir * forward; + jumpVelocity.z = path->spawnArgs.GetFloat( "up", "0" ); + } else { + jumpVelocity = GetJumpVelocity( jumpTarget, TRITE_LEAP_SPEED, 1024.0f ); + } + StopMove(); + TurnToPos( jumpTarget ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + } + case STAGE_TURN: + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + if ( !FacingIdeal() ) { return SRESULT_WAIT; } + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_LEAP; + return SRESULT_WAIT; + case STAGE_LEAP: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + StopMove(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::combat_explode( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LAUNCH, STAGE_MONITOR }; + switch ( parms->stage ) { + case STAGE_START: { + const float t = AnimLength( ANIMCHANNEL_TORSO, "jump_start" ); + const idVec3 enemyOrg = PredictEnemyPos( t ); + const idVec3 org = GetOrigin(); + float maxHeight = ( enemyOrg.z > org.z + 32.0f ) ? enemyOrg.z : ( org.z + 32.0f ); + idVec3 vel = enemyOrg - org; + vel.Normalize(); + vel *= 320.0f; + vel.z = idMath::Sqrt( ( 2.0f * ( maxHeight - org.z ) * 1066.0f ) > 0.0f ? ( 2.0f * ( maxHeight - org.z ) * 1066.0f ) : 0.0f ); + jumpVelocity = vel; + scratchEndTime = maxHeight; + StopMove(); + FaceEnemy(); + waitActionName = "leap_attack_start"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_LAUNCH; + return SRESULT_WAIT; + } + case STAGE_LAUNCH: + if ( !ActionDone( "leap_attack_start" ) ) { return SRESULT_WAIT; } + parms->stage = STAGE_MONITOR; + return SRESULT_WAIT; + case STAGE_MONITOR: { + const idVec3 org = GetOrigin(); + const idVec3 enemyOrg = GetEnemyPos(); + const idVec3 vel = GetLinearVelocity(); + if ( org.z > enemyOrg.z || org.z > scratchEndTime || vel.z <= -10.0f || !InAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack" ) ) { + SetState( "state_Explode" ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::state_LostCombat( stateParms_t* parms ) { + enum { STAGE_START, STAGE_CHASE, STAGE_WANDER }; + switch ( parms->stage ) { + case STAGE_START: + if ( !explode ) { monster_base_lost_combat(); return SRESULT_DONE; } + MoveToEnemy(); + scratchEndTime = DelayTime( 1.5f ); + parms->stage = STAGE_CHASE; + return SRESULT_WAIT; + case STAGE_CHASE: { + if ( CanReachEnemy() ) { SetState( "state_Combat" ); return SRESULT_DONE; } + if ( EnemyRange() < TRITE_EXPLOSION_RADIUS ) { StartAction( "combat_explode" ); return SRESULT_WAIT; } + const int flags = check_attacks(); + if ( flags & ATTACK_LEAP ) { StartAction( "combat_explode" ); return SRESULT_WAIT; } + if ( flags ) { SetState( "state_Combat" ); return SRESULT_DONE; } + if ( MoveDone() ) { Wander(); parms->stage = STAGE_WANDER; } + return SRESULT_WAIT; + } + case STAGE_WANDER: + if ( CanReachEnemy() || Time() >= scratchEndTime ) { SetState( "state_Combat" ); return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::state_Explode( stateParms_t* parms ) { + enum { STAGE_START, STAGE_REMOVE }; + switch ( parms->stage ) { + case STAGE_START: + RadiusDamageFromJoint( "", GetKey( "def_explode_damage", "damage_trite_explode" ) ); + StartSoundKey( "snd_explode", SND_CHANNEL_BODY, false ); + if ( owner ) { owner->ActivateTargets( owner ); } + HideOwner(); + scratchEndTime = DelayTime( 5.0f ); + parms->stage = STAGE_REMOVE; + return SRESULT_WAIT; + case STAGE_REMOVE: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + RemoveOwner(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonTrite::state_Killed( stateParms_t* parms ) { + StopMove(); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 0 ); + waitActionName = "dead"; + SetState( "state_Dead" ); + return SRESULT_DONE; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_trite.h b/neo/doom3/game/ai/AI_monster_demon_trite.h new file mode 100644 index 00000000..ad5b8b52 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_trite.h @@ -0,0 +1,73 @@ +// AI_monster_demon_trite.h + // Native state-machine replacement for ai_monster_demon_trite.script. + + #pragma once + + + #ifndef TRITE_RUNDISTANCE +#define TRITE_RUNDISTANCE 192 +#endif +#ifndef TRITE_WALKTURN +#define TRITE_WALKTURN 65 +#endif +#ifndef TRITE_LEAP_RATE +#define TRITE_LEAP_RATE 2 +#endif +#ifndef TRITE_DODGE_RATE +#define TRITE_DODGE_RATE 2 +#endif +#ifndef TRITE_LEAP_RANGE_MIN +#define TRITE_LEAP_RANGE_MIN 64 +#endif +#ifndef TRITE_LEAP_RANGE_MAX +#define TRITE_LEAP_RANGE_MAX 1024 +#endif +#ifndef TRITE_LEAP_SPEED +#define TRITE_LEAP_SPEED 650 +#endif +#ifndef TRITE_LEAP_MAXHEIGHT +#define TRITE_LEAP_MAXHEIGHT 48 +#endif +#ifndef TRITE_EXPLOSION_RADIUS +#define TRITE_EXPLOSION_RADIUS 200 +#endif + + class rvmMonsterDemonTrite : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonTrite ); + + rvmMonsterDemonTrite(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + bool explode; + float nextDodge; + float nextLeap; + float painNextTime; + idVec3 jumpVelocity; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_LeapAttack( stateParms_t* parms ); + virtual stateResult_t Torso_Walk( stateParms_t* parms ); + virtual stateResult_t Torso_Run( stateParms_t* parms ); + virtual stateResult_t Torso_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Torso_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_leap( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual stateResult_t path_jump( stateParms_t* parms ); + virtual stateResult_t state_LostCombat( stateParms_t* parms ); + virtual stateResult_t state_Explode( stateParms_t* parms ); + virtual stateResult_t combat_explode( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_vulgar.cpp b/neo/doom3/game/ai/AI_monster_demon_vulgar.cpp new file mode 100644 index 00000000..8e46f8d8 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_vulgar.cpp @@ -0,0 +1,428 @@ +// AI_monster_demon_vulgar.cpp + // Native state-machine replacement for ai_monster_demon_vulgar.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonVulgar ) + END_CLASS + + rvmMonsterDemonVulgar::rvmMonsterDemonVulgar() { + nextDodge = 0.0f; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextLeap = 0.0f; + painNextTime = 0.0f; + jumpVelocity = vec3_zero; + combat_node = NULL; +} + +void rvmMonsterDemonVulgar::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + run_distance = VULGAR_RUNDISTANCE; + walk_turn = VULGAR_WALKTURN; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonVulgar::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Torso_LeapAttack", Torso_LeapAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_leap", combat_leap ); + AI_DISPATCH_STATE( "path_jump", path_jump ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonVulgar::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonVulgar::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", VULGAR_PAIN_TO_PAIN ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, VULGAR_PAIN_TO_IDLE ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", VULGAR_PAIN_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, VULGAR_MELEE_TO_IDLE ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", VULGAR_MELEE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, range_attack_anim.c_str() ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + LookAtEnemy( 1.0f ); + if ( AnimDone( ANIMCHANNEL_TORSO, VULGAR_RANGE_TO_IDLE ) ) { + AllowMovement( true ); + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", VULGAR_RANGE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::Torso_TurretAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + AllowMovement( false ); + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "turret_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + FaceEnemy(); + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + AllowMovement( true ); + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::Torso_LeapAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_START_WAIT, STAGE_LOOP, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + OverrideAnim( ANIMCHANNEL_LEGS ); + PreventPain( 1.0f ); + PlayAnim( ANIMCHANNEL_TORSO, "jump_start" ); + parms->stage = STAGE_START_WAIT; + return SRESULT_WAIT; + case STAGE_START_WAIT: + if ( !AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { return SRESULT_WAIT; } + AttackBegin( "melee_vulgarLeapAttack" ); + SetLinearVelocity( jumpVelocity ); + PlayCycle( ANIMCHANNEL_TORSO, "jump_loop" ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( owner && !owner->AI_ONGROUND ) { return SRESULT_WAIT; } + AttackEnd(); + PlayAnim( ANIMCHANNEL_TORSO, "jump_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "leap_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "idle" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonVulgar::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonVulgar::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonVulgar::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonVulgar::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonVulgar::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextDodge = RandomTime( VULGAR_DODGE_RATE ); + nextLeap = RandomTime( VULGAR_LEAP_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonVulgar::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + const float range = EnemyRange(); + if ( range >= VULGAR_LEAP_RANGE_MIN && range < VULGAR_LEAP_RANGE_MAX && currentTime >= nextLeap && CanHitEnemy() ) { + const float t = AnimLength( ANIMCHANNEL_TORSO, "jump_start" ); + const idVec3 jumpTarget = PredictEnemyPos( t ); + jumpVelocity = GetJumpVelocity( jumpTarget, VULGAR_LEAP_SPEED, VULGAR_LEAP_MAXHEIGHT ); + if ( jumpVelocity.LengthSqr() > 0.001f ) { flags |= ATTACK_LEAP; } else { nextLeap = DelayTime( 2.0f ); } + } + if ( !CanReachEnemy() || currentTime >= nextAttack ) { + range_attack_anim = ChooseAnim( ANIMCHANNEL_LEGS, "turret_attack" ); + if ( CanHitEnemyFromAnim( range_attack_anim.c_str() ) ) { flags |= ATTACK_MISSILE; } + const char* movingAnim = ChooseAnim( ANIMCHANNEL_LEGS, "range_attack" ); + if ( TestAnimMoveTowardEnemy( movingAnim ) && CanHitEnemyFromAnim( movingAnim ) ) { + range_attack_anim = movingAnim; + flags |= ATTACK_MISSILE; + } + } + } + return static_cast( flags ); +} + +bool rvmMonsterDemonVulgar::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + VULGAR_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_LEAP ) { StartAction( "combat_leap" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonVulgar::combat_range( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + waitActionName = "range_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "range_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( VULGAR_ATTACK_RATE ); + nextNoFOVAttack = Time() + VULGAR_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::combat_leap( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + TurnToPos( GetOrigin() + jumpVelocity ); + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + nextLeap = DelayTime( VULGAR_LEAP_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::path_jump( stateParms_t* parms ) { + enum { STAGE_PATH_CORNER, STAGE_SETUP, STAGE_TURN, STAGE_LEAP, STAGE_DONE }; + idEntity* path = current_path.GetEntity(); + switch ( parms->stage ) { + case STAGE_PATH_CORNER: + StartAction( "path_corner" ); + parms->stage = STAGE_SETUP; + return SRESULT_WAIT; + case STAGE_SETUP: { + if ( InAction() ) { return SRESULT_WAIT; } + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + idEntity* target = RandomPathFrom( path ); + if ( !target ) { return SRESULT_DONE; } + scratchEntity = target; + const idVec3 jumpTarget = target->GetPhysics()->GetOrigin(); + if ( path && path->spawnArgs.GetString( "up", "" )[0] ) { + float forward = path->spawnArgs.GetFloat( "forward", "0" ); + idVec3 dir = jumpTarget - GetOrigin(); + dir.z = 0.0f; + dir.Normalize(); + jumpVelocity = dir * forward; + jumpVelocity.z = path->spawnArgs.GetFloat( "up", "0" ); + } else { + jumpVelocity = GetJumpVelocity( jumpTarget, VULGAR_LEAP_SPEED, 1024.0f ); + } + StopMove(); + TurnToPos( jumpTarget ); + parms->stage = STAGE_TURN; + return SRESULT_WAIT; + } + case STAGE_TURN: + if ( checkForEnemy( true ) ) { return SRESULT_DONE; } + if ( !FacingIdeal() ) { return SRESULT_WAIT; } + waitActionName = "leap_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_LeapAttack", 4 ); + parms->stage = STAGE_LEAP; + return SRESULT_WAIT; + case STAGE_LEAP: + if ( !ActionDone( "leap_attack" ) ) { return SRESULT_WAIT; } + StopMove(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + StopMove(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( VULGAR_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonVulgar::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + StopMove(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", 2 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( VULGAR_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_vulgar.h b/neo/doom3/game/ai/AI_monster_demon_vulgar.h new file mode 100644 index 00000000..1bf19aa4 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_vulgar.h @@ -0,0 +1,96 @@ +// AI_monster_demon_vulgar.h + // Native state-machine replacement for ai_monster_demon_vulgar.script. + + #pragma once + + + #ifndef VULGAR_RUNDISTANCE +#define VULGAR_RUNDISTANCE 192 +#endif +#ifndef VULGAR_WALKTURN +#define VULGAR_WALKTURN 65 +#endif +#ifndef VULGAR_ATTACK_RATE +#define VULGAR_ATTACK_RATE 1 +#endif +#ifndef VULGAR_DODGE_RATE +#define VULGAR_DODGE_RATE 1 +#endif +#ifndef VULGAR_LEAP_RATE +#define VULGAR_LEAP_RATE 8 +#endif +#ifndef VULGAR_LEAP_SPEED +#define VULGAR_LEAP_SPEED 650 +#endif +#ifndef VULGAR_LEAP_MAXHEIGHT +#define VULGAR_LEAP_MAXHEIGHT 48 +#endif +#ifndef VULGAR_LEAP_RANGE_MIN +#define VULGAR_LEAP_RANGE_MIN 200 +#endif +#ifndef VULGAR_LEAP_RANGE_MAX +#define VULGAR_LEAP_RANGE_MAX 480 +#endif +#ifndef VULGAR_PAIN_DELAY +#define VULGAR_PAIN_DELAY 0.25f +#endif +#ifndef VULGAR_PAIN_TO_IDLE +#define VULGAR_PAIN_TO_IDLE 8 +#endif +#ifndef VULGAR_PAIN_TO_PAIN +#define VULGAR_PAIN_TO_PAIN 4 +#endif +#ifndef VULGAR_MELEE_TO_IDLE +#define VULGAR_MELEE_TO_IDLE 4 +#endif +#ifndef VULGAR_RANGE_TO_IDLE +#define VULGAR_RANGE_TO_IDLE 4 +#endif +#ifndef VULGAR_LEAP_TO_IDLE +#define VULGAR_LEAP_TO_IDLE 4 +#endif +#ifndef VULGAR_NOFOVTIME +#define VULGAR_NOFOVTIME 4 +#endif + + class rvmMonsterDemonVulgar : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonVulgar ); + + rvmMonsterDemonVulgar(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextDodge; + float nextAttack; + float nextNoFOVAttack; + float nextLeap; + float painNextTime; + idVec3 jumpVelocity; + idStr range_attack_anim; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Torso_LeapAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_leap( stateParms_t* parms ); + virtual stateResult_t path_jump( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_demon_wraith.cpp b/neo/doom3/game/ai/AI_monster_demon_wraith.cpp new file mode 100644 index 00000000..4b035ea2 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_wraith.cpp @@ -0,0 +1,332 @@ +// AI_monster_demon_wraith.cpp + // Native state-machine replacement for ai_monster_demon_wraith.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterDemonWraith ) + END_CLASS + + rvmMonsterDemonWraith::rvmMonsterDemonWraith() { + invisible = false; + nextDodge = 0.0f; + nextInvisible = 0.0f; + painNextTime = 0.0f; + combat_node = NULL; + spawn_effect = NULL; +} + +void rvmMonsterDemonWraith::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + run_distance = WRAITH_RUNDISTANCE; + walk_turn = WRAITH_WALKTURN; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterDemonWraith::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "state_LostCombat", state_LostCombat ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + AI_DISPATCH_STATE( "combat_become_visible", combat_become_visible ); + AI_DISPATCH_STATE( "combat_become_invisible", combat_become_invisible ); + AI_DISPATCH_STATE( "spawn_fx", spawn_fx ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterDemonWraith::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", WRAITH_IDLE_TO_PAIN ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonWraith::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( WRAITH_PAIN_DELAY ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", WRAITH_PAIN_TO_PAIN ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, WRAITH_PAIN_TO_IDLE ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", WRAITH_PAIN_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonWraith::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, WRAITH_MELEE_TO_IDLE ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", WRAITH_MELEE_TO_IDLE ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonWraith::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "idle" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", WRAITH_IDLE_TO_RUN ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", WRAITH_IDLE_TO_WALK ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonWraith::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", WRAITH_IDLE_TO_RUN ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", WRAITH_RUN_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonWraith::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", WRAITH_IDLE_TO_WALK ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", WRAITH_RUN_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonWraith::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, WRAITH_DODGE_LEFT_TO_IDLE ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", WRAITH_DODGE_LEFT_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonWraith::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, WRAITH_DODGE_RIGHT_TO_IDLE ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", WRAITH_DODGE_RIGHT_TO_IDLE ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterDemonWraith::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonWraith::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextDodge = RandomTime( WRAITH_DODGE_RATE ); + nextInvisible = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterDemonWraith::check_attacks() { + int flags = 0; + const float range = EnemyRange(); + const float currentTime = Time(); + if ( invisible ) { + if ( ( range < WRAITH_INVISIBLE_DIST_MIN || currentTime >= nextInvisible ) && CanBecomeSolid() ) { flags |= ATTACK_BECOME_VISIBLE; } + return static_cast( flags ); + } + const bool canMelee = CheckForMelee(); + if ( !canMelee && Pain() && currentTime >= nextDodge ) { + if ( TestAnimMove( "evade_left" ) ) { flags |= ATTACK_DODGE_LEFT; } + if ( TestAnimMove( "evade_right" ) ) { + flags |= ATTACK_DODGE_RIGHT; + if ( flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { flags &= ~ATTACK_DODGE_RIGHT; } else { flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + if ( !canMelee ) { + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } + } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( owner && owner->AI_ONGROUND && range > WRAITH_INVISIBLE_DIST_MAX && currentTime >= nextInvisible ) { flags |= ATTACK_BECOME_INVISIBLE; } + return static_cast( flags ); +} + +bool rvmMonsterDemonWraith::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_BECOME_VISIBLE ) { StartAction( "combat_become_visible" ); return true; } + if ( flags & ATTACK_BECOME_INVISIBLE ) { StartAction( "combat_become_invisible" ); return true; } + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + return false; +} + +stateResult_t rvmMonsterDemonWraith::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", WRAITH_WALK_TO_MELEE ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonWraith::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", WRAITH_WALK_TO_DODGE_LEFT ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( WRAITH_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonWraith::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", WRAITH_WALK_TO_DODGE_RIGHT ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( WRAITH_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonWraith::spawn_fx( stateParms_t* parms ) { + // Project-specific effect spawning is intentionally isolated here. + // Bind your wraith_spawneffect entity to spawn_effect if desired. + return SRESULT_DONE; +} + +stateResult_t rvmMonsterDemonWraith::combat_become_visible( stateParms_t* parms ) { + enum { STAGE_START, STAGE_SOLID, STAGE_FADE, STAGE_DONE }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + if ( !invisible ) { nextInvisible = RandomDelay( WRAITH_INVISIBLE_DELAY_MIN, WRAITH_INVISIBLE_DELAY_MAX ); return SRESULT_DONE; } + AllowMovement( false ); + parms->stage = STAGE_SOLID; + return SRESULT_WAIT; + case STAGE_SOLID: + if ( !CanBecomeSolid() ) { return SRESULT_WAIT; } + BecomeSolid(); + if ( owner ) { owner->AllowHiddenMovement( 0 ); } + AllowMovement( true ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartSoundKey( "snd_fade_in", SND_CHANNEL_VOICE2, false ); + ShowOwner(); + scratchEndTime = DelayTime( 0.75f ); + parms->stage = STAGE_FADE; + return SRESULT_WAIT; + case STAGE_FADE: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + ClearBurn(); + invisible = false; + nextInvisible = RandomDelay( WRAITH_INVISIBLE_DELAY_MIN, WRAITH_INVISIBLE_DELAY_MAX ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonWraith::combat_become_invisible( stateParms_t* parms ) { + enum { STAGE_START, STAGE_FADEOUT, STAGE_HIDE }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + if ( invisible ) { return SRESULT_DONE; } + invisible = true; + StartSoundKey( "snd_fade_out", SND_CHANNEL_VOICE2, false ); + scratchEndTime = DelayTime( 0.817f ); + parms->stage = STAGE_FADEOUT; + return SRESULT_WAIT; + case STAGE_FADEOUT: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + PreBurn(); + HideOwner(); + if ( owner ) { owner->AllowHiddenMovement( 1 ); } + nextInvisible = RandomDelay( WRAITH_VISIBLE_DELAY_MIN, WRAITH_VISIBLE_DELAY_MAX ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterDemonWraith::state_LostCombat( stateParms_t* parms ) { + enum { STAGE_INVISIBLE, STAGE_ROUTE, STAGE_VISIBLE }; + switch ( parms->stage ) { + case STAGE_INVISIBLE: + StartAction( "combat_become_invisible" ); + parms->stage = STAGE_ROUTE; + return SRESULT_WAIT; + case STAGE_ROUTE: + if ( InAction() || Time() < nextInvisible ) { return SRESULT_WAIT; } + StopMove(); + if ( !CanReachEnemy() ) { return SRESULT_WAIT; } + scratchEndTime = DelayTime( 2.0f ); + parms->stage = STAGE_VISIBLE; + return SRESULT_WAIT; + case STAGE_VISIBLE: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + StartAction( "combat_become_visible" ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_demon_wraith.h b/neo/doom3/game/ai/AI_monster_demon_wraith.h new file mode 100644 index 00000000..c2e9f6ff --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_demon_wraith.h @@ -0,0 +1,128 @@ +// AI_monster_demon_wraith.h + // Native state-machine replacement for ai_monster_demon_wraith.script. + + #pragma once + + + #ifndef WRAITH_ATTACK_RATE +#define WRAITH_ATTACK_RATE 1.5f +#endif +#ifndef WRAITH_DODGE_RATE +#define WRAITH_DODGE_RATE 5 +#endif +#ifndef WRAITH_PAIN_DELAY +#define WRAITH_PAIN_DELAY 0.25f +#endif +#ifndef WRAITH_RUNDISTANCE +#define WRAITH_RUNDISTANCE 192 +#endif +#ifndef WRAITH_WALKTURN +#define WRAITH_WALKTURN 65 +#endif +#ifndef WRAITH_INVISIBLE_DIST_MIN +#define WRAITH_INVISIBLE_DIST_MIN 140 +#endif +#ifndef WRAITH_INVISIBLE_DIST_MAX +#define WRAITH_INVISIBLE_DIST_MAX 256 +#endif +#ifndef WRAITH_INVISIBLE_DELAY_MIN +#define WRAITH_INVISIBLE_DELAY_MIN 0.6f +#endif +#ifndef WRAITH_INVISIBLE_DELAY_MAX +#define WRAITH_INVISIBLE_DELAY_MAX 1.5f +#endif +#ifndef WRAITH_VISIBLE_DELAY_MIN +#define WRAITH_VISIBLE_DELAY_MIN 1.3f +#endif +#ifndef WRAITH_VISIBLE_DELAY_MAX +#define WRAITH_VISIBLE_DELAY_MAX 2.2f +#endif +#ifndef WRAITH_PAIN_TO_IDLE +#define WRAITH_PAIN_TO_IDLE 2 +#endif +#ifndef WRAITH_PAIN_TO_PAIN +#define WRAITH_PAIN_TO_PAIN 0 +#endif +#ifndef WRAITH_MELEE_TO_IDLE +#define WRAITH_MELEE_TO_IDLE 4 +#endif +#ifndef WRAITH_DODGE_LEFT_TO_IDLE +#define WRAITH_DODGE_LEFT_TO_IDLE 4 +#endif +#ifndef WRAITH_DODGE_RIGHT_TO_IDLE +#define WRAITH_DODGE_RIGHT_TO_IDLE 4 +#endif +#ifndef WRAITH_IDLE_TO_PAIN +#define WRAITH_IDLE_TO_PAIN 0 +#endif +#ifndef WRAITH_IDLE_TO_WALK +#define WRAITH_IDLE_TO_WALK 4 +#endif +#ifndef WRAITH_WALK_TO_IDLE +#define WRAITH_WALK_TO_IDLE 4 +#endif +#ifndef WRAITH_IDLE_TO_RUN +#define WRAITH_IDLE_TO_RUN 4 +#endif +#ifndef WRAITH_WALK_TO_RUN +#define WRAITH_WALK_TO_RUN 4 +#endif +#ifndef WRAITH_RUN_TO_WALK +#define WRAITH_RUN_TO_WALK 4 +#endif +#ifndef WRAITH_RUN_TO_IDLE +#define WRAITH_RUN_TO_IDLE 4 +#endif +#ifndef WRAITH_WALK_TO_MELEE +#define WRAITH_WALK_TO_MELEE 4 +#endif +#ifndef WRAITH_WALK_TO_DODGE_LEFT +#define WRAITH_WALK_TO_DODGE_LEFT 4 +#endif +#ifndef WRAITH_WALK_TO_DODGE_RIGHT +#define WRAITH_WALK_TO_DODGE_RIGHT 4 +#endif +#ifndef ATTACK_BECOME_INVISIBLE +#define ATTACK_BECOME_INVISIBLE ATTACK_SPECIAL1 +#endif +#ifndef ATTACK_BECOME_VISIBLE +#define ATTACK_BECOME_VISIBLE ATTACK_SPECIAL2 +#endif + + class rvmMonsterDemonWraith : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterDemonWraith ); + + rvmMonsterDemonWraith(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + bool invisible; + float nextDodge; + float nextInvisible; + float painNextTime; + idEntityPtr combat_node; + idEntityPtr spawn_effect; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ); + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t state_LostCombat( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + virtual stateResult_t combat_become_visible( stateParms_t* parms ); + virtual stateResult_t combat_become_invisible( stateParms_t* parms ); + virtual stateResult_t spawn_fx( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_flying_cacodemon.cpp b/neo/doom3/game/ai/AI_monster_flying_cacodemon.cpp new file mode 100644 index 00000000..06426eb3 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_flying_cacodemon.cpp @@ -0,0 +1,221 @@ +// AI_monster_flying_cacodemon.cpp + // Native state-machine replacement for ai_monster_flying_cacodemon.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterFlyingCacodemon ) + END_CLASS + + rvmMonsterFlyingCacodemon::rvmMonsterFlyingCacodemon() { + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + painNextTime = 0.0f; + combat_node = NULL; +} + +void rvmMonsterFlyingCacodemon::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterFlyingCacodemon::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Fly", Torso_Fly ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_range", combat_range ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterFlyingCacodemon::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Fly", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterFlyingCacodemon::Torso_Fly( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "fly" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterFlyingCacodemon::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingCacodemon::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingCacodemon::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + FaceEnemy(); + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingCacodemon::Torso_TurretAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PreventPain( 1.0f ); + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + FaceEnemy(); + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingCacodemon::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_FLY ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingCacodemon::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterFlyingCacodemon::check_attacks() { + int flags = 0; + const bool canMelee = CheckForMelee(); + const float currentTime = Time(); + if ( !canMelee ) { combat_node = GetCombatNode(); if ( combat_node.GetEntity() ) { flags |= ATTACK_COMBAT_NODE; } } + if ( canMelee ) { flags |= ATTACK_MELEE; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + if ( !CanReachEnemy() || currentTime >= nextAttack ) { + if ( CanHitEnemyFromAnim( "range_attack" ) ) { flags |= ATTACK_MISSILE; } + } + } + return static_cast( flags ); +} + +bool rvmMonsterFlyingCacodemon::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + CACO_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "combat_range" ); return true; } + return false; +} + +stateResult_t rvmMonsterFlyingCacodemon::combat_range( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + waitActionName = "range_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "range_attack" ) ) { return SRESULT_WAIT; } + nextAttack = DelayTime( CACO_ATTACK_RATE ); + nextNoFOVAttack = Time() + CACO_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingCacodemon::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_flying_cacodemon.h b/neo/doom3/game/ai/AI_monster_flying_cacodemon.h new file mode 100644 index 00000000..4f6df375 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_flying_cacodemon.h @@ -0,0 +1,41 @@ +// AI_monster_flying_cacodemon.h + // Native state-machine replacement for ai_monster_flying_cacodemon.script. + + #pragma once + + + #ifndef CACO_ATTACK_RATE +#define CACO_ATTACK_RATE 3 +#endif +#ifndef CACO_NOFOVTIME +#define CACO_NOFOVTIME 4 +#endif + + class rvmMonsterFlyingCacodemon : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterFlyingCacodemon ); + + rvmMonsterFlyingCacodemon(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextAttack; + float nextNoFOVAttack; + float painNextTime; + idEntityPtr combat_node; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Fly( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_range( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_flying_lostsoul.cpp b/neo/doom3/game/ai/AI_monster_flying_lostsoul.cpp new file mode 100644 index 00000000..6de14193 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_flying_lostsoul.cpp @@ -0,0 +1,242 @@ +// AI_monster_flying_lostsoul.cpp + // Native state-machine replacement for ai_monster_flying_lostsoul.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterFlyingLostsoul ) + END_CLASS + + rvmMonsterFlyingLostsoul::rvmMonsterFlyingLostsoul() { + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + noMeleeTime = 0.0f; + fly_offset = 0.0f; + painNextTime = 0.0f; + chargeVelocity = vec3_zero; +} + +void rvmMonsterFlyingLostsoul::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + fly_offset = GetFloatKey( "fly_offset", "0" ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterFlyingLostsoul::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Fly", Torso_Fly ); + AI_DISPATCH_STATE( "Torso_Charge", Torso_Charge ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_charge", combat_charge ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + AI_DISPATCH_STATE( "combat_retreat", combat_retreat ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterFlyingLostsoul::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Fly", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterFlyingLostsoul::Torso_Fly( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "fly" ); parms->stage = 1; } + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterFlyingLostsoul::Torso_Charge( stateParms_t* parms ) { + enum { STAGE_START, STAGE_CHARGE }; + switch ( parms->stage ) { + case STAGE_START: + PlayCycle( ANIMCHANNEL_TORSO, "charge" ); + AttackBegin( "melee_lostsoul_charge" ); + scratchEndTime = DelayTime( ( EnemyRange() + 256.0f ) / LOSTSOUL_CHARGE_SPEED ); + parms->stage = STAGE_CHARGE; + return SRESULT_WAIT; + case STAGE_CHARGE: + if ( InfluenceActive() || Pain() || Time() >= scratchEndTime ) { + AttackEnd(); + FinishAction( "charge_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + if ( CheckForMelee() ) { AttackMelee( "melee_lostsoul_charge" ); } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingLostsoul::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( Pain() && Time() > painNextTime ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingLostsoul::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_RUNNING }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_RUNNING; + return SRESULT_WAIT; + case STAGE_RUNNING: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingLostsoul::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_FLY ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingLostsoul::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterFlyingLostsoul::check_attacks() { + int flags = 0; + const float currentTime = Time(); + if ( currentTime >= noMeleeTime && CheckForMelee() ) { flags |= ATTACK_MELEE; } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + const float range = EnemyRange(); + if ( range < 40.0f ) { flags |= ATTACK_LOSTSOUL_RETREAT; } + if ( range >= LOSTSOUL_CHARGE_RANGE_MIN && range < LOSTSOUL_CHARGE_RANGE_MAX && ( !CanReachEnemy() || currentTime >= nextAttack ) && owner && owner->TestChargeAttack() > 0.0f ) { + flags |= ATTACK_LOSTSOUL_CHARGE; + } + } + return static_cast( flags ); +} + +bool rvmMonsterFlyingLostsoul::do_attack( int attack_flags ) { + nextNoFOVAttack = Time() + LOSTSOUL_NOFOVTIME; + const int flags = attack_flags; + if ( flags & ATTACK_LOSTSOUL_RETREAT ) { StartAction( "combat_retreat" ); return true; } + if ( flags & ATTACK_MELEE ) { StartAction( "combat_melee" ); return true; } + if ( flags & ATTACK_LOSTSOUL_CHARGE ) { StartAction( "combat_charge" ); return true; } + return false; +} + +stateResult_t rvmMonsterFlyingLostsoul::combat_charge( stateParms_t* parms ) { + enum { STAGE_RISE, STAGE_FACE, STAGE_CHARGE, STAGE_FINISH }; + switch ( parms->stage ) { + case STAGE_RISE: + SetFlyOffset( idMath::Ftoi( fly_offset + 200.0f ) ); + scratchEndTime = DelayTime( 0.5f ); + parms->stage = STAGE_FACE; + return SRESULT_WAIT; + case STAGE_FACE: { + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + FaceEnemy(); + idVec3 forward = GetEnemyPos() - GetOrigin(); + forward.z = 0.0f; + forward.Normalize(); + chargeVelocity = forward * LOSTSOUL_CHARGE_SPEED; + SetFlyOffset( idMath::Ftoi( fly_offset ) ); + SetFlySpeed( LOSTSOUL_CHARGE_SPEED ); + waitActionName = "charge_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Charge", 4 ); + parms->stage = STAGE_CHARGE; + return SRESULT_WAIT; + } + case STAGE_CHARGE: { + if ( !ActionDone( "charge_attack" ) ) { + idVec3 vel = chargeVelocity; + const idVec3 org = GetOrigin(); + const idVec3 pos = GetEnemyPos(); + vel.z = -( org.z - pos.z + 16.0f ) * 3.0f; + SetLinearVelocity( vel ); + return SRESULT_WAIT; + } + SetFlySpeed( LOSTSOUL_NORMAL_SPEED ); + nextAttack = DelayTime( LOSTSOUL_ATTACK_RATE ); + nextNoFOVAttack = Time() + LOSTSOUL_NOFOVTIME; + noMeleeTime = Time() + 1.0f; + return SRESULT_DONE; + } + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingLostsoul::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 4 ); + parms->stage = STAGE_ACTION; + return SRESULT_WAIT; + case STAGE_ACTION: + if ( !ActionDone( "melee_attack" ) ) { return SRESULT_WAIT; } + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterFlyingLostsoul::combat_retreat( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MOVE }; + switch ( parms->stage ) { + case STAGE_START: + MoveOutOfRange( GetEnemy(), EnemyRange() + 256.0f ); + scratchEndTime = DelayTime( 1.0f ); + parms->stage = STAGE_MOVE; + return SRESULT_WAIT; + case STAGE_MOVE: + if ( MoveDone() || Time() >= scratchEndTime ) { return SRESULT_DONE; } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_flying_lostsoul.h b/neo/doom3/game/ai/AI_monster_flying_lostsoul.h new file mode 100644 index 00000000..795130fd --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_flying_lostsoul.h @@ -0,0 +1,61 @@ +// AI_monster_flying_lostsoul.h + // Native state-machine replacement for ai_monster_flying_lostsoul.script. + + #pragma once + + + #ifndef LOSTSOUL_CHARGE_SPEED +#define LOSTSOUL_CHARGE_SPEED 440 +#endif +#ifndef LOSTSOUL_NORMAL_SPEED +#define LOSTSOUL_NORMAL_SPEED 140 +#endif +#ifndef LOSTSOUL_ATTACK_RATE +#define LOSTSOUL_ATTACK_RATE 2 +#endif +#ifndef LOSTSOUL_CHARGE_RANGE_MAX +#define LOSTSOUL_CHARGE_RANGE_MAX 768 +#endif +#ifndef LOSTSOUL_CHARGE_RANGE_MIN +#define LOSTSOUL_CHARGE_RANGE_MIN 64 +#endif +#ifndef LOSTSOUL_NOFOVTIME +#define LOSTSOUL_NOFOVTIME 4 +#endif +#ifndef ATTACK_LOSTSOUL_CHARGE +#define ATTACK_LOSTSOUL_CHARGE ATTACK_SPECIAL1 +#endif +#ifndef ATTACK_LOSTSOUL_RETREAT +#define ATTACK_LOSTSOUL_RETREAT ATTACK_SPECIAL2 +#endif + + class rvmMonsterFlyingLostsoul : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterFlyingLostsoul ); + + rvmMonsterFlyingLostsoul(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + float nextAttack; + float nextNoFOVAttack; + float noMeleeTime; + float fly_offset; + float painNextTime; + idVec3 chargeVelocity; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Fly( stateParms_t* parms ); + virtual stateResult_t Torso_Charge( stateParms_t* parms ); + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_charge( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + virtual stateResult_t combat_retreat( stateParms_t* parms ); + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + + }; diff --git a/neo/doom3/game/ai/AI_monster_turret.cpp b/neo/doom3/game/ai/AI_monster_turret.cpp new file mode 100644 index 00000000..0bcafe4e --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_turret.cpp @@ -0,0 +1,172 @@ +// AI_monster_turret.cpp + // Native state-machine replacement for ai_monster_turret.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + + CLASS_DECLARATION( rvmMonsterBase, rvmMonsterTurret ) + END_CLASS + + rvmMonsterTurret::rvmMonsterTurret() { + fire = false; + attack_monsters = false; + light_is_on = false; + attackTime = 0.0f; + nextFireTime = 0.0f; + shutdownTime = 0.0f; + light = NULL; +} + +void rvmMonsterTurret::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + IgnoreDamage(); + ambush = true; + attack_monsters = GetFloatKey( "team", "1" ) != 1.0f; + if ( GetIntKey( "light" ) ) { StartAction( "spawn_light" ); } + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterTurret::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Death", Torso_Death ); + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Attack", Torso_Attack ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "state_Disabled", state_Disabled ); + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + AI_DISPATCH_STATE( "state_Combat", state_Combat ); + AI_DISPATCH_STATE( "combat_attack", combat_attack ); + AI_DISPATCH_STATE( "spawn_light", spawn_light ); + AI_DISPATCH_STATE( "light_off", light_off ); + AI_DISPATCH_STATE( "light_on", light_on ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterTurret::Torso_Death( stateParms_t* parms ) { + FinishAction( "dead" ); + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterTurret::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_TORSO, "idle" ); parms->stage = 1; } + if ( fire ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Attack", 0 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterTurret::Torso_Attack( stateParms_t* parms ) { + enum { STAGE_WINDUP, STAGE_FIRE, STAGE_WINDDOWN }; + switch ( parms->stage ) { + case STAGE_WINDUP: + PlayCycle( ANIMCHANNEL_TORSO, "idle" ); + StartSoundKey( "snd_windup", SND_CHANNEL_BODY, false ); + attackTime = DelayTime( TURRET_MIN_ATTACK_TIME ); + nextFireTime = Time(); + parms->stage = STAGE_FIRE; + return SRESULT_WAIT; + case STAGE_FIRE: + if ( ( !fire && Time() >= attackTime ) || InfluenceActive() ) { + StartSoundKey( "snd_winddown", SND_CHANNEL_BODY2, false ); + parms->stage = STAGE_WINDDOWN; + scratchEndTime = DelayTime( 0.35f ); + return SRESULT_WAIT; + } + if ( Time() >= nextFireTime ) { + StartSoundKey( "snd_fire", SND_CHANNEL_WEAPON, false ); + AttackMissile( "barrel" ); + nextFireTime = DelayTime( TURRET_FIRE_RATE ); + } + return SRESULT_WAIT; + case STAGE_WINDDOWN: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + FinishAction( "attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterTurret::spawn_light( stateParms_t* parms ) { return SRESULT_DONE; } +stateResult_t rvmMonsterTurret::light_off( stateParms_t* parms ) { light_is_on = false; return SRESULT_DONE; } +stateResult_t rvmMonsterTurret::light_on( stateParms_t* parms ) { light_is_on = true; return SRESULT_DONE; } + +stateResult_t rvmMonsterTurret::state_Begin( stateParms_t* parms ) { + SetMoveType( MOVETYPE_STATIC ); + if ( GetIntKey( "trigger" ) ) { SetState( "state_Disabled" ); return SRESULT_DONE; } + StartAction( "wait_for_enemy" ); + parms->stage = 1; + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterTurret::state_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { StartSoundKey( "snd_shutdown", SND_CHANNEL_VOICE, false ); StartAction( "light_off" ); StartAction( "wait_for_enemy" ); parms->stage = 1; return SRESULT_WAIT; } + if ( InAction() ) { return SRESULT_WAIT; } + SetState( "state_Combat" ); + return SRESULT_DONE; +} + +stateResult_t rvmMonsterTurret::state_Disabled( stateParms_t* parms ) { + enum { STAGE_START, STAGE_TRIGGER }; + switch ( parms->stage ) { + case STAGE_START: + StartSoundKey( "snd_shutdown", SND_CHANNEL_VOICE, false ); + LookAtEnemy( 0.0f ); + attackTime = 0.0f; + fire = false; + ClearEnemy(); + ClearActivated(); + parms->stage = STAGE_TRIGGER; + return SRESULT_WAIT; + case STAGE_TRIGGER: + if ( !Activated() ) { return SRESULT_WAIT; } + ClearActivated(); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterTurret::state_Killed( stateParms_t* parms ) { + StopMove(); + StartAction( "light_off" ); + waitActionName = "dead"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); + StopThinking(); + return SRESULT_WAIT; +} + +bool rvmMonsterTurret::checkForEnemy( bool use_fov ) { + idEntity* enemy = FindEnemy( use_fov ); + if ( !enemy && attack_monsters ) { enemy = FindEnemyAI( use_fov ); } + if ( !enemy ) { return false; } + SetEnemy( enemy ); + return true; +} + +stateResult_t rvmMonsterTurret::state_Combat( stateParms_t* parms ) { + if ( parms->stage == 0 ) { StartSoundKey( "snd_wakeup", SND_CHANNEL_VOICE, false ); StartAction( "light_on" ); shutdownTime = DelayTime( TURRET_SHUTDOWN_TIME ); parms->stage = 1; } + LookAtEnemy( 1.0f ); + if ( EnemyDead() ) { enemy_dead(); return SRESULT_DONE; } + if ( Activated() ) { SetState( "state_Disabled" ); return SRESULT_DONE; } + if ( EnemyVisible() ) { + if ( owner && owner->CanHitEnemyFromJoint( "barrel" ) ) { StartAction( "combat_attack" ); } else { checkForEnemy( false ); } + shutdownTime = DelayTime( TURRET_SHUTDOWN_TIME ); + return SRESULT_WAIT; + } + if ( !checkForEnemy( false ) && Time() >= shutdownTime ) { LookAtEnemy( 0.0f ); ClearEnemy(); SetState( "state_Idle" ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterTurret::combat_attack( stateParms_t* parms ) { + FaceEnemy(); + fire = true; + if ( !EnemyVisible() || !( owner && owner->CanHitEnemyFromJoint( "barrel" ) ) || InfluenceActive() || Activated() ) { + fire = false; + if ( Activated() ) { SetState( "state_Disabled" ); } + return SRESULT_DONE; + } + LookAtEnemy( 1.0f ); + return SRESULT_WAIT; +} diff --git a/neo/doom3/game/ai/AI_monster_turret.h b/neo/doom3/game/ai/AI_monster_turret.h new file mode 100644 index 00000000..56352564 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_turret.h @@ -0,0 +1,48 @@ +// AI_monster_turret.h + // Native state-machine replacement for ai_monster_turret.script. + + #pragma once + + + #ifndef TURRET_FIRE_RATE +#define TURRET_FIRE_RATE 0.1f +#endif +#ifndef TURRET_MIN_ATTACK_TIME +#define TURRET_MIN_ATTACK_TIME 1.5f +#endif +#ifndef TURRET_SHUTDOWN_TIME +#define TURRET_SHUTDOWN_TIME 4 +#endif + + class rvmMonsterTurret : public rvmMonsterBase { + public: + CLASS_PROTOTYPE( rvmMonsterTurret ); + + rvmMonsterTurret(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + + protected: + bool fire; + bool attack_monsters; + bool light_is_on; + float attackTime; + float nextFireTime; + float shutdownTime; + idEntityPtr light; + + virtual stateResult_t Torso_Death( stateParms_t* parms ); + virtual stateResult_t Torso_Idle( stateParms_t* parms ); + virtual stateResult_t Torso_Attack( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ); + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t state_Disabled( stateParms_t* parms ); + virtual stateResult_t state_Killed( stateParms_t* parms ); + virtual stateResult_t state_Combat( stateParms_t* parms ); + virtual stateResult_t combat_attack( stateParms_t* parms ); + virtual stateResult_t spawn_light( stateParms_t* parms ); + virtual stateResult_t light_off( stateParms_t* parms ); + virtual stateResult_t light_on( stateParms_t* parms ); + virtual bool checkForEnemy( bool use_fov ); + + }; diff --git a/neo/doom3/game/ai/AI_monster_zombie.cpp b/neo/doom3/game/ai/AI_monster_zombie.cpp new file mode 100644 index 00000000..9120a958 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie.cpp @@ -0,0 +1,193 @@ +// AI_monster_zombie.cpp +// Native state-machine replacement for ai_monster_zombie.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmMonsterZombieBase, rvmMonsterZombie ) +END_CLASS + +rvmMonsterZombie::rvmMonsterZombie() { + painNextTime = 0.0f; + can_run = false; +} + +void rvmMonsterZombie::Init( idAI* aiOwner ) { + rvmMonsterZombieBase::Init( aiOwner ); + run_distance = 192.0f; + walk_turn = 65.0f; + can_run = HasAnim( ANIMCHANNEL_LEGS, "run" ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterZombie::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterZombieBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterZombie::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = 1; + } + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombie::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( Pain() && Time() > painNextTime ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 8 ); + return SRESULT_DONE; + } + if ( AnimDone( ANIMCHANNEL_TORSO, 3 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombie::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombie::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = 1; + } + if ( can_run && run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombie::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayCycle( ANIMCHANNEL_LEGS, "walk" ); + parms->stage = 1; + } + if ( can_run && run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombie::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayCycle( ANIMCHANNEL_LEGS, "run" ); + parms->stage = 1; + } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombie::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { + return SRESULT_WAIT; + } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombie::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { + return SRESULT_WAIT; + } + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterZombie::check_attacks() { + int attack_flags = 0; + if ( CheckForMelee() ) { + attack_flags |= ATTACK_MELEE; + } + return attack_flags; +} + +bool rvmMonsterZombie::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_MELEE ) { + StartAction( "combat_melee" ); + return true; + } + return false; +} + +stateResult_t rvmMonsterZombie::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 8 ); + parms->stage = STAGE_WAIT_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_ACTION: + if ( !ActionDone( "melee_attack" ) ) { + return SRESULT_WAIT; + } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_zombie.h b/neo/doom3/game/ai/AI_monster_zombie.h new file mode 100644 index 00000000..7ac7824f --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie.h @@ -0,0 +1,31 @@ +// AI_monster_zombie.h +// Native state-machine replacement for ai_monster_zombie.script. + +#pragma once + + +class rvmMonsterZombie : public rvmMonsterZombieBase { +public: + CLASS_PROTOTYPE( rvmMonsterZombie ); + + rvmMonsterZombie(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + float painNextTime; + bool can_run; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ) override; + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ) override; + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ) override; + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; +}; diff --git a/neo/doom3/game/ai/AI_monster_zombie_base.cpp b/neo/doom3/game/ai/AI_monster_zombie_base.cpp new file mode 100644 index 00000000..5b671e63 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_base.cpp @@ -0,0 +1,45 @@ +// AI_monster_zombie_base.cpp +// Native state-machine replacement for ai_monster_zombie_base.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmMonsterBase, rvmMonsterZombieBase ) +END_CLASS + +stateResult_t rvmMonsterZombieBase::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "state_Killed", state_Killed ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +void rvmMonsterZombieBase::archvile_minion() { + if ( owner ) { + gameLocal.Error( "'%s': Zombies cannot be minions for archviles", owner->GetName() ); + } +} + +bool rvmMonsterZombieBase::can_resurrect() { + return false; +} + +stateResult_t rvmMonsterZombieBase::state_Killed( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_DEAD_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + waitActionName = "dead"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 0 ); + parms->stage = STAGE_WAIT_DEAD_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_DEAD_ACTION: + if ( !ActionDone( "dead" ) ) { + return SRESULT_WAIT; + } + StopThinking(); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_zombie_base.h b/neo/doom3/game/ai/AI_monster_zombie_base.h new file mode 100644 index 00000000..fc65de4d --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_base.h @@ -0,0 +1,17 @@ +// AI_monster_zombie_base.h +// Native state-machine replacement for ai_monster_zombie_base.script. + +#pragma once + + +class rvmMonsterZombieBase : public rvmMonsterBase { +public: + CLASS_PROTOTYPE( rvmMonsterZombieBase ); + + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + virtual void archvile_minion() override; + virtual bool can_resurrect() override; + virtual stateResult_t state_Killed( stateParms_t* parms ) override; +}; diff --git a/neo/doom3/game/ai/AI_monster_zombie_bernie.cpp b/neo/doom3/game/ai/AI_monster_zombie_bernie.cpp new file mode 100644 index 00000000..fd70470e --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_bernie.cpp @@ -0,0 +1,176 @@ +// AI_monster_zombie_bernie.cpp +// Native state-machine replacement for ai_monster_zombie_bernie.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmMonsterZombieBase, rvmMonsterZombieBernie ) +END_CLASS + +rvmMonsterZombieBernie::rvmMonsterZombieBernie() { + painNextTime = 0.0f; +} + +void rvmMonsterZombieBernie::Init( idAI* aiOwner ) { + rvmMonsterZombieBase::Init( aiOwner ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterZombieBernie::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterZombieBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterZombieBernie::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = 1; + } + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieBernie::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( Pain() && Time() > painNextTime ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); + return SRESULT_DONE; + } + if ( AnimDone( ANIMCHANNEL_TORSO, 3 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 6 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieBernie::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieBernie::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = 1; + } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieBernie::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayCycle( ANIMCHANNEL_LEGS, "walk" ); + parms->stage = 1; + } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieBernie::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { + return SRESULT_WAIT; + } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieBernie::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { + return SRESULT_WAIT; + } + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterZombieBernie::check_attacks() { + int attack_flags = 0; + if ( CheckForMelee() ) { + attack_flags |= ATTACK_MELEE; + } + return attack_flags; +} + +bool rvmMonsterZombieBernie::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_MELEE ) { + StartAction( "combat_melee" ); + return true; + } + return false; +} + +stateResult_t rvmMonsterZombieBernie::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 8 ); + parms->stage = STAGE_WAIT_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_ACTION: + if ( !ActionDone( "melee_attack" ) ) { + return SRESULT_WAIT; + } + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_zombie_bernie.h b/neo/doom3/game/ai/AI_monster_zombie_bernie.h new file mode 100644 index 00000000..01aa001b --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_bernie.h @@ -0,0 +1,29 @@ +// AI_monster_zombie_bernie.h +// Native state-machine replacement for ai_monster_zombie_bernie.script. + +#pragma once + + +class rvmMonsterZombieBernie : public rvmMonsterZombieBase { +public: + CLASS_PROTOTYPE( rvmMonsterZombieBernie ); + + rvmMonsterZombieBernie(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + float painNextTime; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ) override; + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ) override; + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ) override; + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; +}; diff --git a/neo/doom3/game/ai/AI_monster_zombie_commando_cgun.cpp b/neo/doom3/game/ai/AI_monster_zombie_commando_cgun.cpp new file mode 100644 index 00000000..59ad4c11 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_commando_cgun.cpp @@ -0,0 +1,479 @@ +// AI_monster_zombie_commando_cgun.cpp +// Native state-machine replacement for ai_monster_zombie_commando_cgun.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmMonsterBase, rvmMonsterZombieCommandoCGun ) +END_CLASS + +rvmMonsterZombieCommandoCGun::rvmMonsterZombieCommandoCGun() { + fire = false; + crouch_fire = false; + step_left = false; + step_right = false; + run_attack = false; + nextDodge = 0.0f; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + combat_node = NULL; + painNextTime = 0.0f; + attackEndTime = 0.0f; + loopEndTime = 0.0f; + minShootTime = 0.0f; + firstShot = false; +} + +void rvmMonsterZombieCommandoCGun::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + run_distance = ZOMBIE_CGUN_RUNDISTANCE; + walk_turn = ZOMBIE_CGUN_WALKTURN; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterZombieCommandoCGun::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Torso_CrouchAttack", Torso_CrouchAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "Legs_StepLeft", Legs_StepLeft ); + AI_DISPATCH_STATE( "Legs_StepRight", Legs_StepRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "stand_attack", stand_attack ); + AI_DISPATCH_STATE( "crouch_attack", crouch_attack ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterZombieCommandoCGun::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "range_attack_aim" ); + parms->stage = 1; + } + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); + return SRESULT_DONE; + } + if ( fire ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 8 ); + return SRESULT_DONE; + } + if ( crouch_fire ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_CrouchAttack", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + SetBlendFrames( ANIMCHANNEL_TORSO, 3 ); + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 12 ) ) { + PreventPain( 0.6f ); + FinishAction( "pain" ); + SetBlendFrames( ANIMCHANNEL_TORSO, 12 ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 12 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_INTRO, STAGE_LOOP_START, STAGE_LOOP_WAIT, STAGE_END, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimPrefix( "" ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_INTRO; + return SRESULT_WAIT; + case STAGE_INTRO: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( !AnimDone( ANIMCHANNEL_TORSO, 12 ) ) { return SRESULT_WAIT; } + minShootTime = Time() + ZOMBIE_CGUN_MIN_SHOT_TIME; + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_LOOP_START: + if ( !fire && Time() >= minShootTime ) { parms->stage = STAGE_END; return SRESULT_WAIT; } + if ( InfluenceActive() ) { fire = false; parms->stage = STAGE_END; return SRESULT_WAIT; } + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_loop" ); + parms->stage = STAGE_LOOP_WAIT; + return SRESULT_WAIT; + case STAGE_LOOP_WAIT: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( InfluenceActive() ) { fire = false; parms->stage = STAGE_END; return SRESULT_WAIT; } + if ( !AnimDone( ANIMCHANNEL_TORSO, 12 ) ) { return SRESULT_WAIT; } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_END: + if ( !AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { SetBlendFrames( ANIMCHANNEL_TORSO, 8 ); } + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 3 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 12 ) ) { + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 12 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Torso_TurretAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_INTRO, STAGE_LOOP_START, STAGE_LOOP_WAIT, STAGE_END, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimPrefix( "" ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_INTRO; + return SRESULT_WAIT; + case STAGE_INTRO: + if ( !AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { return SRESULT_WAIT; } + attackEndTime = RandomDelay( ZOMBIE_CGUN_STAND_ATTACK_MIN_LENGTH, ZOMBIE_CGUN_STAND_ATTACK_MAX_LENGTH ); + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_LOOP_START: + if ( Time() >= attackEndTime || !CanHitEnemyFromAnim( "range_attack_loop" ) || InfluenceActive() ) { parms->stage = STAGE_END; return SRESULT_WAIT; } + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_loop" ); + parms->stage = STAGE_LOOP_WAIT; + return SRESULT_WAIT; + case STAGE_LOOP_WAIT: + if ( !AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { return SRESULT_WAIT; } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_END: + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Torso_CrouchAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_INTRO, STAGE_LOOP_START, STAGE_LOOP_WAIT, STAGE_AIM, STAGE_END, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + OverrideAnim( ANIMCHANNEL_LEGS ); + SetAnimPrefix( "crouch" ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_INTRO; + return SRESULT_WAIT; + case STAGE_INTRO: + if ( !AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { return SRESULT_WAIT; } + minShootTime = Time() + ZOMBIE_CGUN_MIN_SHOT_TIME; + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_LOOP_START: + if ( !crouch_fire && Time() >= minShootTime ) { parms->stage = STAGE_END; return SRESULT_WAIT; } + loopEndTime = RandomDelay( ZOMBIE_CGUN_STAND_ATTACK_MIN_LENGTH, ZOMBIE_CGUN_STAND_ATTACK_MAX_LENGTH ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_loop" ); + parms->stage = STAGE_LOOP_WAIT; + return SRESULT_WAIT; + case STAGE_LOOP_WAIT: + if ( InfluenceActive() ) { crouch_fire = false; parms->stage = STAGE_END; return SRESULT_WAIT; } + if ( !AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { return SRESULT_WAIT; } + if ( !crouch_fire || Time() >= loopEndTime ) { + PlayCycle( ANIMCHANNEL_TORSO, "range_attack_aim" ); + loopEndTime = RandomDelay( ZOMBIE_CGUN_WAIT_MIN_LENGTH, ZOMBIE_CGUN_WAIT_MAX_LENGTH ); + parms->stage = STAGE_AIM; + return SRESULT_WAIT; + } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_AIM: + if ( !crouch_fire || InfluenceActive() ) { crouch_fire = false; parms->stage = STAGE_END; return SRESULT_WAIT; } + if ( Time() < loopEndTime ) { return SRESULT_WAIT; } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_END: + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { + FinishAction( "crouch_attack" ); + SetAnimPrefix( "" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "range_attack_aim" ); parms->stage = 1; } + if ( GetEnemy() ) { run = true; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + if ( step_left ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_StepLeft", 8 ); return SRESULT_DONE; } + if ( step_right ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_StepRight", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( GetEnemy() ) { run = true; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( GetEnemy() ) { run = true; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 8 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 8 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Legs_StepLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "step_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 8 ) ) { step_left = false; FinishAction( "sidestep" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoCGun::Legs_StepRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "step_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 8 ) ) { step_right = false; FinishAction( "sidestep" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoCGun::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + fire = false; crouch_fire = false; step_left = false; step_right = false; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoCGun::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextDodge = RandomTime( ZOMBIE_CGUN_DODGE_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterZombieCommandoCGun::check_attacks() { + int attack_flags = 0; + const float currentTime = Time(); + if ( Pain() && currentTime >= nextDodge && owner ) { + if ( owner->TestAnimMove( "evade_left" ) ) { attack_flags |= ATTACK_DODGE_LEFT; } + if ( owner->TestAnimMove( "evade_right" ) ) { + attack_flags |= ATTACK_DODGE_RIGHT; + if ( attack_flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { attack_flags &= ~ATTACK_DODGE_RIGHT; } else { attack_flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { attack_flags |= ATTACK_COMBAT_NODE; } + + const bool reachable = CanReachEnemy(); + bool try_attack = false; + if ( EnemyInFOV() && ( !reachable || currentTime >= nextAttack ) ) { + try_attack = true; + } else if ( Time() > nextNoFOVAttack && EnemyVisible() && ( !reachable || currentTime >= nextAttack ) ) { + try_attack = true; + } else if ( EnemyRange() < 64.0f ) { + try_attack = true; + } + + if ( try_attack ) { + if ( RandomFloat() < 0.5f ) { + if ( CanHitEnemyFromAnim( "crouch_range_attack_loop" ) ) { attack_flags |= ATTACK_ZCC_CROUCHFIRE; } + else if ( CanHitEnemyFromAnim( "range_attack_loop" ) ) { attack_flags |= ATTACK_MISSILE; } + } else if ( CanHitEnemyFromAnim( "range_attack_loop" ) ) { + attack_flags |= ATTACK_MISSILE; + } + } + return attack_flags; +} + +bool rvmMonsterZombieCommandoCGun::do_attack( int attack_flags ) { + const int flags = attack_flags; + nextNoFOVAttack = Time() + ZOMBIE_CGUN_NOFOVTIME; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_ZCC_CROUCHFIRE ) { StartAction( "crouch_attack" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "stand_attack" ); return true; } + return false; +} + +stateResult_t rvmMonsterZombieCommandoCGun::stand_attack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP, STAGE_FINISH }; + switch ( parms->stage ) { + case STAGE_START: + fire = true; + attackEndTime = RandomDelay( ZOMBIE_CGUN_STAND_ATTACK_MIN_LENGTH, ZOMBIE_CGUN_STAND_ATTACK_MAX_LENGTH ); + loopEndTime = RandomDelay( 0.5f, 1.0f ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( InfluenceActive() || Time() > attackEndTime || !EnemyVisible() || !CanHitEnemyFromAnim( "range_attack_loop" ) ) { + fire = false; + parms->stage = STAGE_FINISH; + return SRESULT_WAIT; + } + LookAtEnemy( 1.0f ); + if ( !run_attack || EnemyRange() < 90.0f ) { + FaceEnemy(); + } else if ( !EntityInAttackCone( GetEnemy() ) ) { + fire = false; + parms->stage = STAGE_FINISH; + return SRESULT_WAIT; + } else { + MoveToEnemy(); + } + if ( Time() >= loopEndTime ) { + loopEndTime = RandomDelay( 0.5f, 1.0f ); + if ( owner ) { + const bool left = owner->TestAnimMove( "step_left" ) != 0; + const bool right = owner->TestAnimMove( "step_right" ) != 0; + if ( left && right ) { + step_left = RandomFloat( 100.0f ) >= 50.0f; + step_right = !step_left; + } else { + step_left = left; + step_right = right; + } + } + } + return SRESULT_WAIT; + case STAGE_FINISH: + fire = false; + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack" ) ) { return SRESULT_WAIT; } + nextAttack = RandomDelay( ZOMBIE_CGUN_ATTACK_DELAY_MIN, ZOMBIE_CGUN_ATTACK_DELAY_MAX ); + nextNoFOVAttack = Time() + ZOMBIE_CGUN_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoCGun::crouch_attack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP, STAGE_FINISH }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + crouch_fire = true; + attackEndTime = RandomDelay( ZOMBIE_CGUN_CROUCH_ATTACK_MIN_LENGTH, ZOMBIE_CGUN_CROUCH_ATTACK_MAX_LENGTH ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( InfluenceActive() || Time() > attackEndTime || !EnemyVisible() || !CanHitEnemyFromAnim( "range_attack_loop" ) ) { + crouch_fire = false; + SetAnimPrefix( "" ); + parms->stage = STAGE_FINISH; + return SRESULT_WAIT; + } + LookAtEnemy( 1.0f ); + return SRESULT_WAIT; + case STAGE_FINISH: + crouch_fire = false; + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_CrouchAttack" ) ) { return SRESULT_WAIT; } + nextAttack = RandomDelay( ZOMBIE_CGUN_ATTACK_DELAY_MIN, ZOMBIE_CGUN_ATTACK_DELAY_MAX ); + nextNoFOVAttack = Time() + ZOMBIE_CGUN_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoCGun::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", 8 ); + parms->stage = STAGE_WAIT_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( ZOMBIE_CGUN_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoCGun::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", 8 ); + parms->stage = STAGE_WAIT_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( ZOMBIE_CGUN_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_zombie_commando_cgun.h b/neo/doom3/game/ai/AI_monster_zombie_commando_cgun.h new file mode 100644 index 00000000..3cab79d6 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_commando_cgun.h @@ -0,0 +1,69 @@ +// AI_monster_zombie_commando_cgun.h +// Native state-machine replacement for ai_monster_zombie_commando_cgun.script. + +#pragma once + + +#ifndef ZOMBIE_CGUN_RUNDISTANCE +#define ZOMBIE_CGUN_RUNDISTANCE 192 +#define ZOMBIE_CGUN_WALKTURN 65 +#define ZOMBIE_CGUN_DODGE_RATE 3 +#define ZOMBIE_CGUN_NOFOVTIME 4 +#define ZOMBIE_CGUN_MIN_SHOT_TIME 0.5 +#define ZOMBIE_CGUN_ATTACK_DELAY_MIN 0.5 +#define ZOMBIE_CGUN_ATTACK_DELAY_MAX 2 +#define ZOMBIE_CGUN_STAND_ATTACK_MIN_LENGTH 3 +#define ZOMBIE_CGUN_STAND_ATTACK_MAX_LENGTH 15 +#define ZOMBIE_CGUN_CROUCH_ATTACK_MIN_LENGTH 3 +#define ZOMBIE_CGUN_CROUCH_ATTACK_MAX_LENGTH 15 +#define ZOMBIE_CGUN_WAIT_MIN_LENGTH 0.3 +#define ZOMBIE_CGUN_WAIT_MAX_LENGTH 1.5 +#define ATTACK_ZCC_CROUCHFIRE ATTACK_SPECIAL1 +#endif + +class rvmMonsterZombieCommandoCGun : public rvmMonsterBase { +public: + CLASS_PROTOTYPE( rvmMonsterZombieCommandoCGun ); + + rvmMonsterZombieCommandoCGun(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + bool fire; + bool crouch_fire; + bool step_left; + bool step_right; + bool run_attack; + float nextDodge; + float nextAttack; + float nextNoFOVAttack; + idEntityPtr combat_node; + float painNextTime; + float attackEndTime; + float loopEndTime; + float minShootTime; + bool firstShot; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ) override; + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Torso_CrouchAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ) override; + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t Legs_StepLeft( stateParms_t* parms ); + virtual stateResult_t Legs_StepRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ) override; + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t stand_attack( stateParms_t* parms ); + virtual stateResult_t crouch_attack( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; +}; diff --git a/neo/doom3/game/ai/AI_monster_zombie_commando_tentacle.cpp b/neo/doom3/game/ai/AI_monster_zombie_commando_tentacle.cpp new file mode 100644 index 00000000..090dff28 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_commando_tentacle.cpp @@ -0,0 +1,256 @@ +// AI_monster_zombie_commando_tentacle.cpp +// Native state-machine replacement for ai_monster_zombie_commando_tentacle.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmMonsterBase, rvmMonsterZombieCommandoTentacle ) +END_CLASS + +rvmMonsterZombieCommandoTentacle::rvmMonsterZombieCommandoTentacle() { + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + tentacleDamage = false; + painNextTime = 0.0f; +} + +void rvmMonsterZombieCommandoTentacle::Init( idAI* aiOwner ) { + rvmMonsterBase::Init( aiOwner ); + run_distance = ZCT_RUNDISTANCE; + walk_turn = ZCT_WALKTURN; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterZombieCommandoTentacle::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Torso_TentacleAttack", Torso_TentacleAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_tentacle", combat_tentacle ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterZombieCommandoTentacle::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = 1; + } + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( Pain() && Time() > painNextTime ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 0 ); + return SRESULT_DONE; + } + if ( AnimDone( ANIMCHANNEL_TORSO, 2 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 2 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::Torso_TentacleAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + FaceEnemy(); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "tentacle_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = 1; + } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 4 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + tentacleDamage = false; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterZombieCommandoTentacle::check_attacks() { + int attack_flags = 0; + if ( CheckForMelee() ) { + attack_flags |= ATTACK_MELEE; + } + if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { + const float range = EnemyRange(); + if ( range < ZCT_TENTACLE_RANGE_MAX && ( !CanReachEnemy() || Time() >= nextAttack ) && CanHitEnemy() ) { + attack_flags |= ATTACK_TENTACLE; + } + } + return attack_flags; +} + +bool rvmMonsterZombieCommandoTentacle::do_attack( int attack_flags ) { + const int flags = attack_flags; + nextNoFOVAttack = Time() + ZCT_NOFOVTIME; + if ( flags & ATTACK_MELEE ) { + StartAction( "combat_melee" ); + return true; + } + if ( flags & ATTACK_TENTACLE ) { + StartAction( "combat_tentacle" ); + return true; + } + return false; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::combat_tentacle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP }; + switch ( parms->stage ) { + case STAGE_START: + tentacleDamage = false; + StopMove(); + waitActionName = "tentacle_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_TentacleAttack", 4 ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( tentacleDamage && owner && owner->MeleeAttackToJoint( "joint13", "melee_commandoTentacle" ) ) { + tentacleDamage = false; + } + if ( !InAnimState( ANIMCHANNEL_TORSO, "Torso_TentacleAttack" ) || ActionDone( "tentacle_attack" ) ) { + tentacleDamage = false; + nextAttack = DelayTime( ZCT_ATTACK_RATE ); + nextNoFOVAttack = Time() + ZCT_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieCommandoTentacle::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP }; + switch ( parms->stage ) { + case STAGE_START: + tentacleDamage = false; + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 5 ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + LookAtEnemy( 1.0f ); + if ( tentacleDamage && owner && owner->MeleeAttackToJoint( "joint13", "melee_commandoTentacle" ) ) { + tentacleDamage = false; + } + if ( !InAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack" ) || ActionDone( "melee_attack" ) ) { + tentacleDamage = false; + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +void rvmMonsterZombieCommandoTentacle::tentacle_attack_start() { + tentacleDamage = true; +} + +void rvmMonsterZombieCommandoTentacle::tentacle_attack_end() { + tentacleDamage = false; +} diff --git a/neo/doom3/game/ai/AI_monster_zombie_commando_tentacle.h b/neo/doom3/game/ai/AI_monster_zombie_commando_tentacle.h new file mode 100644 index 00000000..71fb3bed --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_commando_tentacle.h @@ -0,0 +1,46 @@ +// AI_monster_zombie_commando_tentacle.h +// Native state-machine replacement for ai_monster_zombie_commando_tentacle.script. + +#pragma once + + +#ifndef ZCT_RUNDISTANCE +#define ZCT_RUNDISTANCE 128 +#define ZCT_WALKTURN 65 +#define ZCT_ATTACK_RATE 5 +#define ZCT_TENTACLE_RANGE_MAX 200 +#define ZCT_NOFOVTIME 4 +#define ATTACK_TENTACLE ATTACK_SPECIAL1 +#endif + +class rvmMonsterZombieCommandoTentacle : public rvmMonsterBase { +public: + CLASS_PROTOTYPE( rvmMonsterZombieCommandoTentacle ); + + rvmMonsterZombieCommandoTentacle(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + float nextAttack; + float nextNoFOVAttack; + bool tentacleDamage; + float painNextTime; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ) override; + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TentacleAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ) override; + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ) override; + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_tentacle( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + virtual void tentacle_attack_start(); + virtual void tentacle_attack_end(); +}; diff --git a/neo/doom3/game/ai/AI_monster_zombie_morgue.cpp b/neo/doom3/game/ai/AI_monster_zombie_morgue.cpp new file mode 100644 index 00000000..ba9a0598 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_morgue.cpp @@ -0,0 +1,176 @@ +// AI_monster_zombie_morgue.cpp +// Native state-machine replacement for ai_monster_zombie_morgue.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmMonsterZombieBase, rvmMonsterZombieMorgue ) +END_CLASS + +rvmMonsterZombieMorgue::rvmMonsterZombieMorgue() { + painNextTime = 0.0f; +} + +void rvmMonsterZombieMorgue::Init( idAI* aiOwner ) { + rvmMonsterZombieBase::Init( aiOwner ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterZombieMorgue::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterZombieBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterZombieMorgue::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "idle" ); + parms->stage = 1; + } + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieMorgue::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( Pain() && Time() > painNextTime ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 8 ); + return SRESULT_DONE; + } + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieMorgue::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieMorgue::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_LEGS, "idle" ); + parms->stage = 1; + } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieMorgue::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayCycle( ANIMCHANNEL_LEGS, "run" ); + parms->stage = 1; + } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieMorgue::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { + return SRESULT_WAIT; + } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieMorgue::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { + return SRESULT_WAIT; + } + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterZombieMorgue::check_attacks() { + int attack_flags = 0; + if ( CheckForMelee() ) { + attack_flags |= ATTACK_MELEE; + } + return attack_flags; +} + +bool rvmMonsterZombieMorgue::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_MELEE ) { + StartAction( "combat_melee" ); + return true; + } + return false; +} + +stateResult_t rvmMonsterZombieMorgue::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 8 ); + parms->stage = STAGE_WAIT_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_ACTION: + if ( !ActionDone( "melee_attack" ) ) { + return SRESULT_WAIT; + } + LookAtEnemy( 8.0f ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_zombie_morgue.h b/neo/doom3/game/ai/AI_monster_zombie_morgue.h new file mode 100644 index 00000000..4c2cda54 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_morgue.h @@ -0,0 +1,29 @@ +// AI_monster_zombie_morgue.h +// Native state-machine replacement for ai_monster_zombie_morgue.script. + +#pragma once + + +class rvmMonsterZombieMorgue : public rvmMonsterZombieBase { +public: + CLASS_PROTOTYPE( rvmMonsterZombieMorgue ); + + rvmMonsterZombieMorgue(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + float painNextTime; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ) override; + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ) override; + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ) override; + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; +}; diff --git a/neo/doom3/game/ai/AI_monster_zombie_sawyer.cpp b/neo/doom3/game/ai/AI_monster_zombie_sawyer.cpp new file mode 100644 index 00000000..ef822afc --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_sawyer.cpp @@ -0,0 +1,216 @@ +// AI_monster_zombie_sawyer.cpp +// Native state-machine replacement for ai_monster_zombie_sawyer.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmMonsterZombieBase, rvmMonsterZombieSawyer ) +END_CLASS + +rvmMonsterZombieSawyer::rvmMonsterZombieSawyer() { + attack = false; + painNextTime = 0.0f; + nextHitTime = 0.0f; + bloodSmokeFrames = 0; +} + +void rvmMonsterZombieSawyer::Init( idAI* aiOwner ) { + rvmMonsterZombieBase::Init( aiOwner ); + SetSmokeVisibility( -1, false ); + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterZombieSawyer::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_MeleeAttack", Torso_MeleeAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "combat_melee", combat_melee ); + return rvmMonsterZombieBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterZombieSawyer::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "stand" ); + parms->stage = 1; + } + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSawyer::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( Pain() && Time() > painNextTime ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 4 ); + return SRESULT_DONE; + } + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "pain" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSawyer::Torso_MeleeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + PlayAnim( ANIMCHANNEL_TORSO, "melee_attack" ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { + FinishAction( "melee_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSawyer::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_LEGS, "stand" ); + parms->stage = 1; + } + if ( MovingForward() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSawyer::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + PlayCycle( ANIMCHANNEL_LEGS, "run" ); + parms->stage = 1; + } + if ( !MovingForward() ) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSawyer::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { + return SRESULT_WAIT; + } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSawyer::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { + return SRESULT_WAIT; + } + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterZombieSawyer::check_attacks() { + int attack_flags = 0; + if ( CheckForMelee() ) { + attack_flags |= ATTACK_MELEE; + } + return attack_flags; +} + +bool rvmMonsterZombieSawyer::do_attack( int attack_flags ) { + const int flags = attack_flags; + if ( flags & ATTACK_MELEE ) { + StartAction( "combat_melee" ); + return true; + } + return false; +} + +stateResult_t rvmMonsterZombieSawyer::combat_melee( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP }; + switch ( parms->stage ) { + case STAGE_START: + LookAtEnemy( 100.0f ); + FaceEnemy(); + nextHitTime = 0.0f; + bloodSmokeFrames = 0; + attack = false; + waitActionName = "melee_attack"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack", 8 ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( attack && Time() > nextHitTime ) { + if ( AttackMelee( "melee_zombie_chainsaw" ) ) { + nextHitTime = Time() + 0.025f; + SetSmokeVisibility( SAWYER_BLOOD_PARTICLES, true ); + bloodSmokeFrames = 4; + } + } + if ( bloodSmokeFrames > 0 ) { + --bloodSmokeFrames; + if ( bloodSmokeFrames == 0 ) { + SetSmokeVisibility( SAWYER_BLOOD_PARTICLES, false ); + } + } + if ( !InAnimState( ANIMCHANNEL_TORSO, "Torso_MeleeAttack" ) || ActionDone( "melee_attack" ) ) { + SetSmokeVisibility( SAWYER_BLOOD_PARTICLES, false ); + attack = false; + LookAtEnemy( 1.0f ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +void rvmMonsterZombieSawyer::start_smoke() { + SetSmokeVisibility( SAWYER_SMOKE_PARTICLES, true ); +} + +void rvmMonsterZombieSawyer::start_attack() { + attack = true; +} + +void rvmMonsterZombieSawyer::end_attack() { + attack = false; +} diff --git a/neo/doom3/game/ai/AI_monster_zombie_sawyer.h b/neo/doom3/game/ai/AI_monster_zombie_sawyer.h new file mode 100644 index 00000000..e5719b38 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_sawyer.h @@ -0,0 +1,40 @@ +// AI_monster_zombie_sawyer.h +// Native state-machine replacement for ai_monster_zombie_sawyer.script. + +#pragma once + + +#ifndef SAWYER_SMOKE_PARTICLES +#define SAWYER_SMOKE_PARTICLES 0 +#define SAWYER_BLOOD_PARTICLES 1 +#endif + +class rvmMonsterZombieSawyer : public rvmMonsterZombieBase { +public: + CLASS_PROTOTYPE( rvmMonsterZombieSawyer ); + + rvmMonsterZombieSawyer(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + bool attack; + float painNextTime; + float nextHitTime; + int bloodSmokeFrames; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ) override; + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_MeleeAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ) override; + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ) override; + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t combat_melee( stateParms_t* parms ); + + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; + virtual void start_smoke(); + virtual void start_attack(); + virtual void end_attack(); +}; diff --git a/neo/doom3/game/ai/AI_monster_zombie_security_pistol.cpp b/neo/doom3/game/ai/AI_monster_zombie_security_pistol.cpp new file mode 100644 index 00000000..d5e87d27 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_security_pistol.cpp @@ -0,0 +1,485 @@ +// AI_monster_zombie_security_pistol.cpp +// Native state-machine replacement for ai_monster_zombie_security_pistol.script. + +#include "precompiled.h" +#pragma hdrstop +#include "../Game_local.h" + + +CLASS_DECLARATION( rvmMonsterZombieBase, rvmMonsterZombieSecurityPistol ) +END_CLASS + +static float zsecp_num_stand_attacks = 1.0f; +static float zsecp_num_crouch_attacks = 1.0f; + +rvmMonsterZombieSecurityPistol::rvmMonsterZombieSecurityPistol() { + fire = false; + crouch_fire = false; + step_left = false; + step_right = false; + run_attack = false; + nextDodge = 0.0f; + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + combat_node = NULL; + painNextTime = 0.0f; + attackEndTime = 0.0f; + loopEndTime = 0.0f; + minShootTime = 0.0f; + firstShot = false; +} + +void rvmMonsterZombieSecurityPistol::Init( idAI* aiOwner ) { + rvmMonsterZombieBase::Init( aiOwner ); + run_distance = ZSECP_RUNDISTANCE; + walk_turn = ZSECP_WALKTURN; + run_attack = GetIntKey( "run_attack" ) != 0; + SetState( "state_Begin" ); +} + +stateResult_t rvmMonsterZombieSecurityPistol::RunStateByName( const char* stateName, stateParms_t* parms ) { + AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); + AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); + AI_DISPATCH_STATE( "Torso_RangeAttack", Torso_RangeAttack ); + AI_DISPATCH_STATE( "Torso_TurretAttack", Torso_TurretAttack ); + AI_DISPATCH_STATE( "Torso_CrouchAttack", Torso_CrouchAttack ); + AI_DISPATCH_STATE( "Legs_Idle", Legs_Idle ); + AI_DISPATCH_STATE( "Legs_Walk", Legs_Walk ); + AI_DISPATCH_STATE( "Legs_Run", Legs_Run ); + AI_DISPATCH_STATE( "Legs_DodgeLeft", Legs_DodgeLeft ); + AI_DISPATCH_STATE( "Legs_DodgeRight", Legs_DodgeRight ); + AI_DISPATCH_STATE( "Legs_StepLeft", Legs_StepLeft ); + AI_DISPATCH_STATE( "Legs_StepRight", Legs_StepRight ); + AI_DISPATCH_STATE( "state_Begin", state_Begin ); + AI_DISPATCH_STATE( "state_Idle", state_Idle ); + AI_DISPATCH_STATE( "stand_attack", stand_attack ); + AI_DISPATCH_STATE( "crouch_attack", crouch_attack ); + AI_DISPATCH_STATE( "combat_dodge_left", combat_dodge_left ); + AI_DISPATCH_STATE( "combat_dodge_right", combat_dodge_right ); + return rvmMonsterZombieBase::RunStateByName( stateName, parms ); +} + +stateResult_t rvmMonsterZombieSecurityPistol::Torso_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { + IdleAnim( ANIMCHANNEL_TORSO, "stand" ); + parms->stage = 1; + } + if ( Pain() ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); + return SRESULT_DONE; + } + if ( fire ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack", 0 ); + return SRESULT_DONE; + } + if ( crouch_fire ) { + SetAnimState( ANIMCHANNEL_TORSO, "Torso_CrouchAttack", 0 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Torso_Pain( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + SetBlendFrames( ANIMCHANNEL_TORSO, 2 ); + PlayAnim( ANIMCHANNEL_TORSO, GetPainAnim() ); + painNextTime = DelayTime( 0.25f ); + parms->stage = STAGE_WAIT; + return SRESULT_WAIT; + case STAGE_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + PreventPain( 0.6f ); + FinishAction( "pain" ); + SetBlendFrames( ANIMCHANNEL_TORSO, 4 ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Torso_RangeAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_INTRO, STAGE_LOOP_START, STAGE_LOOP_WAIT, STAGE_END, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimPrefix( "" ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_INTRO; + return SRESULT_WAIT; + case STAGE_INTRO: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { return SRESULT_WAIT; } + minShootTime = Time() + ZSECP_MIN_SHOT_TIME; + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_LOOP_START: + if ( !fire && Time() >= minShootTime ) { parms->stage = STAGE_END; return SRESULT_WAIT; } + if ( InfluenceActive() ) { fire = false; parms->stage = STAGE_END; return SRESULT_WAIT; } + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_loop" ); + parms->stage = STAGE_LOOP_WAIT; + return SRESULT_WAIT; + case STAGE_LOOP_WAIT: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( InfluenceActive() ) { fire = false; parms->stage = STAGE_END; return SRESULT_WAIT; } + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { return SRESULT_WAIT; } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_END: + if ( !AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { SetBlendFrames( ANIMCHANNEL_TORSO, 8 ); } + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( Pain() ) { SetAnimState( ANIMCHANNEL_TORSO, "Torso_Pain", 2 ); return SRESULT_DONE; } + if ( AnimDone( ANIMCHANNEL_TORSO, 4 ) ) { + FinishAction( "range_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Torso_TurretAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_INTRO, STAGE_LOOP_START, STAGE_LOOP_WAIT, STAGE_END, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + SetAnimPrefix( "" ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_INTRO; + return SRESULT_WAIT; + case STAGE_INTRO: + if ( !AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { return SRESULT_WAIT; } + attackEndTime = RandomDelay( ZSECP_STAND_ATTACK_MIN_LENGTH, ZSECP_STAND_ATTACK_MAX_LENGTH ); + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_LOOP_START: + if ( Time() >= attackEndTime || !CanHitEnemyFromAnim( "range_attack_loop" ) || InfluenceActive() ) { parms->stage = STAGE_END; return SRESULT_WAIT; } + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_loop" ); + parms->stage = STAGE_LOOP_WAIT; + return SRESULT_WAIT; + case STAGE_LOOP_WAIT: + if ( !AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { return SRESULT_WAIT; } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_END: + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 8 ) ) { + FinishAction( "turret_attack" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Torso_CrouchAttack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_INTRO, STAGE_LOOP_START, STAGE_LOOP_WAIT, STAGE_AIM, STAGE_END, STAGE_END_WAIT }; + switch ( parms->stage ) { + case STAGE_START: + OverrideAnim( ANIMCHANNEL_LEGS ); + SetAnimPrefix( "crouch" ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack" ); + parms->stage = STAGE_INTRO; + return SRESULT_WAIT; + case STAGE_INTRO: + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { return SRESULT_WAIT; } + minShootTime = Time() + ZSECP_MIN_SHOT_TIME; + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_LOOP_START: + if ( !crouch_fire && Time() >= minShootTime ) { parms->stage = STAGE_END; return SRESULT_WAIT; } + loopEndTime = RandomDelay( ZSECP_STAND_ATTACK_MIN_LENGTH, ZSECP_STAND_ATTACK_MAX_LENGTH ); + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_loop" ); + parms->stage = STAGE_LOOP_WAIT; + return SRESULT_WAIT; + case STAGE_LOOP_WAIT: + if ( InfluenceActive() ) { crouch_fire = false; parms->stage = STAGE_END; return SRESULT_WAIT; } + if ( !AnimDone( ANIMCHANNEL_TORSO, 0 ) ) { return SRESULT_WAIT; } + if ( !crouch_fire || Time() >= loopEndTime ) { + PlayCycle( ANIMCHANNEL_TORSO, "range_attack_aim" ); + loopEndTime = RandomDelay( ZSECP_WAIT_MIN_LENGTH, ZSECP_WAIT_MAX_LENGTH ); + parms->stage = STAGE_AIM; + return SRESULT_WAIT; + } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_AIM: + if ( !crouch_fire || InfluenceActive() ) { crouch_fire = false; parms->stage = STAGE_END; return SRESULT_WAIT; } + if ( Time() < loopEndTime ) { return SRESULT_WAIT; } + parms->stage = STAGE_LOOP_START; + return SRESULT_WAIT; + case STAGE_END: + PlayAnim( ANIMCHANNEL_TORSO, "range_attack_end" ); + parms->stage = STAGE_END_WAIT; + return SRESULT_WAIT; + case STAGE_END_WAIT: + if ( AnimDone( ANIMCHANNEL_TORSO, 6 ) ) { + FinishAction( "crouch_attack" ); + SetAnimPrefix( "" ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 6 ); + return SRESULT_DONE; + } + return SRESULT_WAIT; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Legs_Idle( stateParms_t* parms ) { + if ( parms->stage == 0 ) { IdleAnim( ANIMCHANNEL_LEGS, "stand" ); parms->stage = 1; } + if ( GetEnemy() ) { run = true; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); return SRESULT_DONE; } + if ( MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + if ( step_left ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_StepLeft", 8 ); return SRESULT_DONE; } + if ( step_right ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_StepRight", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Legs_Walk( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "walk" ); parms->stage = 1; } + if ( GetEnemy() ) { run = true; } + if ( run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Run", 8 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Legs_Run( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayCycle( ANIMCHANNEL_LEGS, "run" ); parms->stage = 1; } + if ( GetEnemy() ) { run = true; } + if ( !run && MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Walk", 8 ); return SRESULT_DONE; } + if ( !MovingForward() ) { SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 8 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Legs_DodgeLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 2 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 2 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Legs_DodgeRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "evade_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 2 ) ) { FinishAction( "strafe" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 2 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Legs_StepLeft( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "step_left" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { step_left = false; FinishAction( "sidestep" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSecurityPistol::Legs_StepRight( stateParms_t* parms ) { + if ( parms->stage == 0 ) { PlayAnim( ANIMCHANNEL_LEGS, "step_right" ); parms->stage = 1; } + if ( AnimDone( ANIMCHANNEL_LEGS, 4 ) ) { step_right = false; FinishAction( "sidestep" ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); return SRESULT_DONE; } + return SRESULT_WAIT; +} + +stateResult_t rvmMonsterZombieSecurityPistol::state_Begin( stateParms_t* parms ) { + enum { STAGE_START, STAGE_MONSTER_BEGIN }; + switch ( parms->stage ) { + case STAGE_START: + fire = false; crouch_fire = false; step_left = false; step_right = false; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + StartAction( "monster_begin" ); + parms->stage = STAGE_MONSTER_BEGIN; + return SRESULT_WAIT; + case STAGE_MONSTER_BEGIN: + if ( InAction() ) { return SRESULT_WAIT; } + SetMoveType( MOVETYPE_ANIM ); + SetState( "state_Idle" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSecurityPistol::state_Idle( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_FOR_ENEMY }; + switch ( parms->stage ) { + case STAGE_START: + StartAction( "wait_for_enemy" ); + parms->stage = STAGE_WAIT_FOR_ENEMY; + return SRESULT_WAIT; + case STAGE_WAIT_FOR_ENEMY: + if ( InAction() ) { return SRESULT_WAIT; } + nextAttack = 0.0f; + nextNoFOVAttack = 0.0f; + nextDodge = RandomTime( ZSECP_DODGE_RATE ); + SetState( "state_Combat" ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +int rvmMonsterZombieSecurityPistol::check_attacks() { + int attack_flags = 0; + const float currentTime = Time(); + if ( Pain() && currentTime >= nextDodge && owner ) { + if ( owner->TestAnimMove( "evade_left" ) ) { attack_flags |= ATTACK_DODGE_LEFT; } + if ( owner->TestAnimMove( "evade_right" ) ) { + attack_flags |= ATTACK_DODGE_RIGHT; + if ( attack_flags & ATTACK_DODGE_LEFT ) { + if ( RandomFloat( 100.0f ) < 50.0f ) { attack_flags &= ~ATTACK_DODGE_RIGHT; } else { attack_flags &= ~ATTACK_DODGE_LEFT; } + } + } + } + + combat_node = GetCombatNode(); + if ( combat_node.GetEntity() ) { attack_flags |= ATTACK_COMBAT_NODE; } + + const bool reachable = CanReachEnemy(); + bool try_attack = false; + if ( EnemyInFOV() && ( !reachable || currentTime >= nextAttack ) ) { + try_attack = true; + } else if ( Time() > nextNoFOVAttack && EnemyVisible() && ( !reachable || currentTime >= nextAttack ) ) { + try_attack = true; + } else if ( EnemyRange() < 64.0f ) { + try_attack = true; + } + + if ( try_attack ) { + if ( RandomFloat( zsecp_num_stand_attacks + zsecp_num_crouch_attacks ) < zsecp_num_stand_attacks ) { + if ( CanHitEnemyFromAnim( "crouch_range_attack_loop" ) ) { attack_flags |= ATTACK_ZSECP_CROUCHFIRE; } + else if ( CanHitEnemyFromAnim( "range_attack_loop" ) ) { attack_flags |= ATTACK_MISSILE; } + } else if ( CanHitEnemyFromAnim( "range_attack_loop" ) ) { + attack_flags |= ATTACK_MISSILE; + } + } + return attack_flags; +} + +bool rvmMonsterZombieSecurityPistol::do_attack( int attack_flags ) { + const int flags = attack_flags; + nextNoFOVAttack = Time() + ZSECP_NOFOVTIME; + if ( flags & ATTACK_DODGE_LEFT ) { StartAction( "combat_dodge_left" ); return true; } + if ( flags & ATTACK_DODGE_RIGHT ) { StartAction( "combat_dodge_right" ); return true; } + if ( flags & ATTACK_COMBAT_NODE ) { scratchEntity = combat_node.GetEntity(); StartAction( "combat_ainode" ); return true; } + if ( flags & ATTACK_ZSECP_CROUCHFIRE ) { StartAction( "crouch_attack" ); return true; } + if ( flags & ATTACK_MISSILE ) { StartAction( "stand_attack" ); return true; } + return false; +} + +stateResult_t rvmMonsterZombieSecurityPistol::stand_attack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP, STAGE_FINISH }; + switch ( parms->stage ) { + case STAGE_START: + zsecp_num_stand_attacks += 1.0f; + fire = true; + attackEndTime = RandomDelay( ZSECP_STAND_ATTACK_MIN_LENGTH, ZSECP_STAND_ATTACK_MAX_LENGTH ); + loopEndTime = RandomDelay( 0.5f, 1.0f ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( InfluenceActive() || Time() > attackEndTime || !EnemyVisible() || !CanHitEnemyFromAnim( "range_attack_loop" ) ) { + fire = false; + parms->stage = STAGE_FINISH; + return SRESULT_WAIT; + } + LookAtEnemy( 1.0f ); + if ( !run_attack || EnemyRange() < 90.0f ) { + FaceEnemy(); + } else if ( !EntityInAttackCone( GetEnemy() ) ) { + fire = false; + parms->stage = STAGE_FINISH; + return SRESULT_WAIT; + } else { + MoveToEnemy(); + } + if ( Time() >= loopEndTime ) { + loopEndTime = RandomDelay( 0.5f, 1.0f ); + if ( owner ) { + const bool left = owner->TestAnimMove( "step_left" ) != 0; + const bool right = owner->TestAnimMove( "step_right" ) != 0; + if ( left && right ) { + step_left = RandomFloat( 100.0f ) >= 50.0f; + step_right = !step_left; + } else { + step_left = left; + step_right = right; + } + } + } + return SRESULT_WAIT; + case STAGE_FINISH: + fire = false; + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_RangeAttack" ) ) { return SRESULT_WAIT; } + nextAttack = RandomDelay( ZSECP_ATTACK_DELAY_MIN, ZSECP_ATTACK_DELAY_MAX ); + nextNoFOVAttack = Time() + ZSECP_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSecurityPistol::crouch_attack( stateParms_t* parms ) { + enum { STAGE_START, STAGE_LOOP, STAGE_FINISH }; + switch ( parms->stage ) { + case STAGE_START: + zsecp_num_crouch_attacks += 1.0f; + FaceEnemy(); + crouch_fire = true; + attackEndTime = RandomDelay( ZSECP_CROUCH_ATTACK_MIN_LENGTH, ZSECP_CROUCH_ATTACK_MAX_LENGTH ); + parms->stage = STAGE_LOOP; + return SRESULT_WAIT; + case STAGE_LOOP: + if ( InfluenceActive() || Time() > attackEndTime || !EnemyVisible() || !CanHitEnemyFromAnim( "range_attack_loop" ) ) { + crouch_fire = false; + SetAnimPrefix( "" ); + parms->stage = STAGE_FINISH; + return SRESULT_WAIT; + } + LookAtEnemy( 1.0f ); + return SRESULT_WAIT; + case STAGE_FINISH: + crouch_fire = false; + if ( InAnimState( ANIMCHANNEL_TORSO, "Torso_CrouchAttack" ) ) { return SRESULT_WAIT; } + nextAttack = RandomDelay( ZSECP_ATTACK_DELAY_MIN, ZSECP_ATTACK_DELAY_MAX ); + nextNoFOVAttack = Time() + ZSECP_NOFOVTIME; + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSecurityPistol::combat_dodge_left( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeLeft", 2 ); + parms->stage = STAGE_WAIT_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( ZSECP_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} + +stateResult_t rvmMonsterZombieSecurityPistol::combat_dodge_right( stateParms_t* parms ) { + enum { STAGE_START, STAGE_WAIT_ACTION }; + switch ( parms->stage ) { + case STAGE_START: + StopMove(); + FaceEnemy(); + waitActionName = "strafe"; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_DodgeRight", 2 ); + parms->stage = STAGE_WAIT_ACTION; + return SRESULT_WAIT; + case STAGE_WAIT_ACTION: + if ( !ActionDone( "strafe" ) ) { return SRESULT_WAIT; } + nextDodge = DelayTime( ZSECP_DODGE_RATE ); + return SRESULT_DONE; + } + return SRESULT_ERROR; +} diff --git a/neo/doom3/game/ai/AI_monster_zombie_security_pistol.h b/neo/doom3/game/ai/AI_monster_zombie_security_pistol.h new file mode 100644 index 00000000..75c05cc7 --- /dev/null +++ b/neo/doom3/game/ai/AI_monster_zombie_security_pistol.h @@ -0,0 +1,69 @@ +// AI_monster_zombie_security_pistol.h +// Native state-machine replacement for ai_monster_zombie_security_pistol.script. + +#pragma once + + +#ifndef ZSECP_RUNDISTANCE +#define ZSECP_RUNDISTANCE 192 +#define ZSECP_WALKTURN 65 +#define ZSECP_DODGE_RATE 3 +#define ZSECP_NOFOVTIME 4 +#define ZSECP_MIN_SHOT_TIME 0.5 +#define ZSECP_ATTACK_DELAY_MIN 0.5 +#define ZSECP_ATTACK_DELAY_MAX 2 +#define ZSECP_STAND_ATTACK_MIN_LENGTH 3 +#define ZSECP_STAND_ATTACK_MAX_LENGTH 15 +#define ZSECP_CROUCH_ATTACK_MIN_LENGTH 3 +#define ZSECP_CROUCH_ATTACK_MAX_LENGTH 15 +#define ZSECP_WAIT_MIN_LENGTH 0.3 +#define ZSECP_WAIT_MAX_LENGTH 1.5 +#define ATTACK_ZSECP_CROUCHFIRE ATTACK_SPECIAL1 +#endif + +class rvmMonsterZombieSecurityPistol : public rvmMonsterZombieBase { +public: + CLASS_PROTOTYPE( rvmMonsterZombieSecurityPistol ); + + rvmMonsterZombieSecurityPistol(); + virtual void Init( idAI* aiOwner ) override; + virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; + +protected: + bool fire; + bool crouch_fire; + bool step_left; + bool step_right; + bool run_attack; + float nextDodge; + float nextAttack; + float nextNoFOVAttack; + idEntityPtr combat_node; + float painNextTime; + float attackEndTime; + float loopEndTime; + float minShootTime; + bool firstShot; + + virtual stateResult_t Torso_Idle( stateParms_t* parms ) override; + virtual stateResult_t Torso_Pain( stateParms_t* parms ); + virtual stateResult_t Torso_RangeAttack( stateParms_t* parms ); + virtual stateResult_t Torso_TurretAttack( stateParms_t* parms ); + virtual stateResult_t Torso_CrouchAttack( stateParms_t* parms ); + virtual stateResult_t Legs_Idle( stateParms_t* parms ) override; + virtual stateResult_t Legs_Walk( stateParms_t* parms ); + virtual stateResult_t Legs_Run( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeLeft( stateParms_t* parms ); + virtual stateResult_t Legs_DodgeRight( stateParms_t* parms ); + virtual stateResult_t Legs_StepLeft( stateParms_t* parms ); + virtual stateResult_t Legs_StepRight( stateParms_t* parms ); + virtual stateResult_t state_Begin( stateParms_t* parms ) override; + virtual stateResult_t state_Idle( stateParms_t* parms ); + virtual stateResult_t stand_attack( stateParms_t* parms ); + virtual stateResult_t crouch_attack( stateParms_t* parms ); + virtual stateResult_t combat_dodge_left( stateParms_t* parms ); + virtual stateResult_t combat_dodge_right( stateParms_t* parms ); + + virtual int check_attacks() override; + virtual bool do_attack( int attack_flags ) override; +};