// AI_monster_base.cpp // Native state-machine replacement for ai_monster_base.script. #include "precompiled.h" #pragma hdrstop #include "../Game_local.h" 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 ) { const idEventDef* activateEvent = ent->FindEventDef( "activate", "e" ); if ( activateEvent ) { ent->ProcessEvent( activateEvent, 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" ); }