/* ** Command & Conquer Renegade(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . */ /*********************************************************************************************** *** Confidential - Westwood Studios *** *********************************************************************************************** * * * Project Name : Commando * * * * $Archive:: /Commando/Code/Combat/soldierobserver.cpp $* * * * $Author:: Byon_g $* * * * $Modtime:: 12/17/01 3:06p $* * * * $Revision:: 112 $* * * *---------------------------------------------------------------------------------------------* * Functions: * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include "soldierobserver.h" #include "soldier.h" #include "action.h" #include "debug.h" #include "crandom.h" #include "cover.h" #include "wwaudio.h" #include "persistfactory.h" #include "combatchunkid.h" #include "combat.h" #include "gameobjobserver.h" #include "gameobjmanager.h" #include "weapons.h" #include "conversationmgr.h" #include "pathfind.h" #include "timemgr.h" #include "weaponbag.h" #include "wwprofile.h" #include "playertype.h" #define THINK_RATE 1 #define THINK_ID 100123 #define INNATE_ACTION_ID 9000000 /* ** */ const char * SoldierAIStateNames[SoldierObserverClass::NUM_SOLDIER_AI_STATES] = { "Relaxed Idle", //SOLDIER_AI_RELAXED_IDLE = 0, "Alert Idle", //SOLDIER_AI_ALERT_IDLE, "Footsteps Heard", //SOLDIER_AI_FOOTSTEPS_HEARD, "Bullet Heard", //SOLDIER_AI_BULLET_HEARD, "Gunshot Heard", //SOLDIER_AI_GUNSHOT_HEARD, "Enemy Seen" //SOLDIER_AI_ENEMY_SEEN, }; int StatePriorities[ SoldierObserverClass::NUM_SOLDIER_AI_STATES ] = { 10, // SOLDIER_AI_RELAXED_IDLE = 0, 20, // SOLDIER_AI_ALERTED_IDLE, 50, // SOLDIER_AI_FOOTSTEPS_HEARD, 50, // SOLDIER_AI_BULLET_HEARD, 70, // SOLDIER_AI_GUNSHOT_HEARD, 90, // SOLDIER_AI_ENEMY_SEEN, }; typedef struct { float StateDuration; // How long we should stay in this state int NextState; // State to enter when we complete this state } SoldierAIStateData; SoldierAIStateData StateData[SoldierObserverClass::NUM_SOLDIER_AI_STATES] = { {9999, SoldierObserverClass::SOLDIER_AI_RELAXED_IDLE}, {180, SoldierObserverClass::SOLDIER_AI_RELAXED_IDLE}, {6, SoldierObserverClass::SOLDIER_AI_CONDITIONAL_IDLE}, {20, SoldierObserverClass::SOLDIER_AI_ALERT_IDLE}, {60, SoldierObserverClass::SOLDIER_AI_ALERT_IDLE}, // {60, SoldierObserverClass::SOLDIER_AI_BULLET_HEARD}, {60, SoldierObserverClass::SOLDIER_AI_BULLET_HEARD}, }; /* ** Speech */ const char* _SpeakEnemySeen[] = { "ThereHeIs02", "KillHim02", "GetHim02", "ForKane02", }; const int _SpeakEnemySeenCount = (sizeof(_SpeakEnemySeen) / sizeof(_SpeakEnemySeen[0])) ; const char* Pick_Speak_EnemySeen(void) { int index = FreeRandom.Get_Int(_SpeakEnemySeenCount); return _SpeakEnemySeen[index]; } Vector3 Random_Vector(float size) { Vector3 vect( FreeRandom.Get_Float( -size, size ), FreeRandom.Get_Float( -size, size ), 0 ); return vect; } /* ** */ SoldierObserverClass::SoldierObserverClass( void ) : State( SOLDIER_AI_RELAXED_IDLE ), StateTimer( 0 ), HomeRadius( 9999999 ), ActionTimer( 0 ), CoverPosition( NULL ), CoveredAttack( false ), LastEvent( SOLDIER_AI_RELAXED_IDLE ), ConversationTimer( WWMath::Random_Float( 1.0F, 10.0F ) ), IsAlerted( false ), Aggressiveness( 0.5f ), TakeCoverProbability( 0.5f ), IsStationary( false ), LastWeaponIndex( 0 ) { } SoldierObserverClass::~SoldierObserverClass( void ) { Release_Cover_Position(); } /* ** */ void SoldierObserverClass::Release_Cover_Position( void ) { if ( CoverPosition != NULL ) { CoverManager::Release_Cover( CoverPosition ); CoverPosition = NULL; } } /* ** */ SimplePersistFactoryClass _SoldierObserverPersistFactory; const PersistFactoryClass & SoldierObserverClass::Get_Factory (void) const { return _SoldierObserverPersistFactory; } enum { CHUNKID_PARENT = 410001836, CHUNKID_VARIABLES, CHUNKID_ENEMY_OBJ_REF, MICROCHUNKID_STATE = 1, MICROCHUNKID_STATE_TIMER, MICROCHUNKID_HOME_LOCATION, MICROCHUNKID_ALERT_POSITION, MICROCHUNKID_ACTION_TIMER, XXXMICROCHUNKID_ANIMATING, XXXMICROCHUNKID_MOVING, MICROCHUNKID_COVER_POSITION, MICROCHUNKID_COVERED_ATTACK, MICROCHUNKID_HOME_RADIUS, XXXMICROCHUNKID_ESCORT_OFFSET, MICROCHUNKID_CONVERSATION_TIMER, MICROCHUNKID_IS_ALERTED, MICROCHUNKID_AGGRESSIVENESS, MICROCHUNKID_TAKE_COVER_PROBABILITY, MICROCHUNKID_IS_STATIONARY, MICROCHUNKID_LAST_WEAPON_INDEX, }; bool SoldierObserverClass::Save (ChunkSaveClass &csave) { csave.Begin_Chunk( CHUNKID_PARENT ); PersistentGameObjObserverClass::Save( csave ); csave.End_Chunk(); if ( CombatManager::Are_Observers_Active() ) { csave.Begin_Chunk( CHUNKID_VARIABLES ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_STATE, State ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_STATE_TIMER, StateTimer ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_HOME_LOCATION, HomeLocation ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_HOME_RADIUS, HomeRadius ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_ALERT_POSITION, AlertPosition ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_ACTION_TIMER, ActionTimer ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_COVER_POSITION, CoverPosition ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_COVERED_ATTACK, CoveredAttack ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_CONVERSATION_TIMER, ConversationTimer ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_IS_ALERTED, IsAlerted ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_AGGRESSIVENESS, Aggressiveness ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_TAKE_COVER_PROBABILITY, TakeCoverProbability ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_IS_STATIONARY, IsStationary ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_LAST_WEAPON_INDEX, LastWeaponIndex ); csave.End_Chunk(); if ( EnemyObject != NULL ) { csave.Begin_Chunk( CHUNKID_ENEMY_OBJ_REF ); EnemyObject.Save( csave ); csave.End_Chunk(); } } // Don't need to save SubStateString, or LastEvent return true; } bool SoldierObserverClass::Load (ChunkLoadClass &cload) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case CHUNKID_PARENT: PersistentGameObjObserverClass::Load( cload ); break; case CHUNKID_VARIABLES: while (cload.Open_Micro_Chunk()) { switch(cload.Cur_Micro_Chunk_ID()) { READ_MICRO_CHUNK( cload, MICROCHUNKID_STATE, State ); READ_MICRO_CHUNK( cload, MICROCHUNKID_STATE_TIMER, StateTimer ); READ_MICRO_CHUNK( cload, MICROCHUNKID_HOME_LOCATION, HomeLocation ); READ_MICRO_CHUNK( cload, MICROCHUNKID_HOME_RADIUS, HomeRadius ); READ_MICRO_CHUNK( cload, MICROCHUNKID_ALERT_POSITION, AlertPosition ); READ_MICRO_CHUNK( cload, MICROCHUNKID_ACTION_TIMER, ActionTimer ); READ_MICRO_CHUNK( cload, MICROCHUNKID_COVER_POSITION, CoverPosition ); READ_MICRO_CHUNK( cload, MICROCHUNKID_COVERED_ATTACK, CoveredAttack ); READ_MICRO_CHUNK( cload, MICROCHUNKID_CONVERSATION_TIMER, ConversationTimer ); READ_MICRO_CHUNK( cload, MICROCHUNKID_IS_ALERTED, IsAlerted ); READ_MICRO_CHUNK( cload, MICROCHUNKID_AGGRESSIVENESS, Aggressiveness ); READ_MICRO_CHUNK( cload, MICROCHUNKID_TAKE_COVER_PROBABILITY, TakeCoverProbability ); READ_MICRO_CHUNK( cload, MICROCHUNKID_IS_STATIONARY, IsStationary ); READ_MICRO_CHUNK( cload, MICROCHUNKID_LAST_WEAPON_INDEX, LastWeaponIndex ); default: Debug_Say(("Unhandled Variable Chunk:%d File:%s Line:%d\r\n",cload.Cur_Micro_Chunk_ID(),__FILE__,__LINE__)); break; } cload.Close_Micro_Chunk(); } break; case CHUNKID_ENEMY_OBJ_REF: EnemyObject.Load( cload ); break; default: Debug_Say(("Unhandled Chunk:%d File:%s Line:%d\r\n",cload.Cur_Chunk_ID(),__FILE__,__LINE__)); break; } cload.Close_Chunk(); } if ( CoverPosition ) { REQUEST_POINTER_REMAP( (void **)&CoverPosition ); } return true; } /* ** */ void SoldierObserverClass::Attach( GameObject * obj ) { // Warning, Attach may not be called on loaded scripts } void SoldierObserverClass::Detach( GameObject * obj ) { // // Clear the soldier's internal innate observer pointer // SmartGameObj *smart_game_obj = obj->As_SmartGameObj(); if ( smart_game_obj != NULL ) { SoldierGameObj *soldier = smart_game_obj->As_SoldierGameObj(); if ( soldier != NULL ) { if ( soldier->Get_Innate_Observer () == this ) { soldier->Clear_Innate_Observer(); } } } GameObjObserverManager::Delete_Register( this ); } void SoldierObserverClass::Created( GameObject* obj ) { SmartGameObj* smart = obj->As_SmartGameObj(); WWASSERT( smart != NULL ); // Debug_Say(("Innate soldier [%d] created\n", obj->Get_ID())); smart->Get_Position( &HomeLocation ); // Enable enemy sensory smart->Set_Enemy_Seen_Enabled(true); obj->Start_Observer_Timer( Get_ID(), THINK_RATE, THINK_ID ); Aggressiveness = smart->As_SoldierGameObj()->Get_Definition().InnateAggressiveness; // Modify aggressiveness based on difficulty switch ( CombatManager::Get_Difficulty_Level() ) { case 0: if ( Aggressiveness > 0.25f ) { Aggressiveness -= 0.25f; } break; case 1: // no change break; case 2: Aggressiveness += 0.25f; if ( Aggressiveness > 1 ) { Aggressiveness = 1; } break; }; TakeCoverProbability = smart->As_SoldierGameObj()->Get_Definition().InnateTakeCoverProbability; IsStationary = smart->As_SoldierGameObj()->Get_Definition().InnateIsStationary; } void SoldierObserverClass::Destroyed(GameObject* obj) { // Debug_Say(("Innate soldier [%d] destroyed\n", obj->Get_ID())); Release_Cover_Position(); } void SoldierObserverClass::Timer_Expired(GameObject* obj, int timer_id) { SmartGameObj* smart = obj->As_SmartGameObj(); WWASSERT( smart != NULL ); SoldierGameObj* soldier = smart->As_SoldierGameObj(); WWASSERT(soldier != NULL); if (timer_id == THINK_ID) { obj->Start_Observer_Timer( Get_ID(), THINK_RATE, THINK_ID ); Think(soldier); } } void SoldierObserverClass::Killed(GameObject* obj, GameObject* killer) { // Debug_Say(("Innate soldier [%d] killed by [%d]\n", obj->Get_ID(), killer ? killer->Get_ID() : -1)); } void SoldierObserverClass::Damaged(GameObject* obj, GameObject* damager, float amount) { // Debug_Say(("Innate soldier [%d] damaged by [%d]\n", obj->Get_ID(), damager ? damager->Get_ID() : -1)); if ( obj->Is_Hibernating() ) { return; } SmartGameObj* smart = obj->As_SmartGameObj(); WWASSERT( smart != NULL ); SoldierGameObj* soldier = smart->As_SoldierGameObj(); WWASSERT(soldier != NULL); if (soldier->Is_Innate_Enabled(SOLDIER_INNATE_EVENT_BULLET_HEARD)) { //Debug_Say(( "Damaged\n" )); Vector3 pos; soldier->Get_Position( &pos ); if ( damager != NULL ) { if ( FreeRandom.Get_Float() < 0.8 ) { // 80% chance of knowing the source damager->Get_Position( &pos ); } } Set_State(soldier, SOLDIER_AI_BULLET_HEARD, pos); } // If I am attacking an object, and I am damaged by a closer object, switch targets if ( State == SOLDIER_AI_ENEMY_SEEN ) { GameObject * enemy = EnemyObject; if ( damager != NULL && enemy != damager ) { Vector3 my_pos; soldier->Get_Position( &my_pos ); Vector3 pos; float target_range = 1000000; if ( enemy ) { enemy->Get_Position( &pos ); pos -= my_pos; target_range = pos.Length(); } damager->Get_Position( &pos ); pos -= my_pos; if ( pos.Length() < target_range ) { PhysicalGameObj * pdamager = damager->As_PhysicalGameObj(); if ( pdamager && pdamager->Is_Enemy( soldier ) ) { Debug_Say(( "Switching to nearer target\n" )); EnemyObject = damager; ActionTimer = 0; // Act on it now } } } } } void SoldierObserverClass::Sound_Heard(GameObject* obj, const CombatSound& sound) { if ( obj->Is_Hibernating() ) { return; } SmartGameObj* smart = obj->As_SmartGameObj(); WWASSERT( smart != NULL ); SoldierGameObj* soldier = smart->As_SoldierGameObj(); WWASSERT(soldier != NULL); PhysicalGameObj * creator = NULL; if ( sound.Creator != NULL ) { creator = sound.Creator->As_PhysicalGameObj(); } bool state_changed = false; switch (sound.Type) { case SOUND_TYPE_BULLET_HIT: { if (soldier->Is_Innate_Enabled(SOLDIER_INNATE_EVENT_BULLET_HEARD)) { Vector3 pos; pos = sound.Position; if ( creator && FreeRandom.Get_Float() < 0.7 ) { // 70% chance of knowing the source creator->Get_Position( &pos ); } state_changed = Set_State(soldier, SOLDIER_AI_BULLET_HEARD, pos); } } break; case SOUND_TYPE_GUNSHOT: // Don't hear friendly if ( (creator == NULL) || !soldier->Is_Teammate( creator ) ) { if (soldier->Is_Innate_Enabled(SOLDIER_INNATE_EVENT_GUNSHOT_HEARD)) { state_changed = Set_State(soldier, SOLDIER_AI_GUNSHOT_HEARD, sound.Position); } } break; case SOUND_TYPE_FOOTSTEPS: if ( (creator == NULL) || !soldier->Is_Teammate( creator ) ) { if (soldier->Is_Innate_Enabled(SOLDIER_INNATE_EVENT_FOOTSTEP_HEARD)) { //Debug_Say(( "Heard Footsteps\n" )); state_changed = Set_State(soldier, SOLDIER_AI_FOOTSTEPS_HEARD, sound.Position); } } break; default: // Debug_Say(("Innate soldier [%d] unrecognized sound!\n", obj->Get_ID())); break; } if ( state_changed ) { Notify_Neighbors_Sound( soldier, sound ); } } void SoldierObserverClass::Enemy_Seen(GameObject* obj, GameObject* enemy) { if ( obj->Is_Hibernating() ) { return; } // WWASSERT( obj != enemy ); // only see the enemy if it has health WWASSERT( enemy ); PhysicalGameObj * p_enemy = enemy->As_PhysicalGameObj(); if ( p_enemy == NULL || p_enemy->Get_Defense_Object()->Get_Health() <= 0 ) { // Debug_Say(( "I see dead people\n" )); return; } GameObject * curr_enemy = EnemyObject; if ( curr_enemy != NULL && curr_enemy != enemy ) { // take the closer enemy Vector3 old_pos; curr_enemy->Get_Position( &old_pos ); Vector3 new_pos; enemy->Get_Position( &new_pos ); Vector3 my_pos; obj->Get_Position( &my_pos ); old_pos -= my_pos; new_pos -= my_pos; if ( new_pos.Length2() > old_pos.Length2() ) { // Debug_Say(( "New guy is too far away!\n" )); return; } // Debug_Say(( "Found New guy!!\n" )); } SmartGameObj* smart = obj->As_SmartGameObj(); WWASSERT( smart != NULL ); SoldierGameObj* soldier = smart->As_SoldierGameObj(); WWASSERT(soldier != NULL); // Debug_Say(("Innate soldier [%d] seen enemy [%d]\n", obj->Get_ID(), enemy->Get_ID())); bool state_changed = false; if (soldier->Is_Innate_Enabled(SOLDIER_INNATE_EVENT_ENEMY_SEEN)) { //Debug_Say(( "Seen Enemy\n" )); state_changed = Set_State(soldier, SOLDIER_AI_ENEMY_SEEN, Vector3(0,0,0), enemy); } if ( state_changed ) { Notify_Neighbors_Enemy( soldier, enemy ); } } void SoldierObserverClass::Custom( GameObject * obj, int type, int param, GameObject * sender ) { if ( type == CUSTOM_EVENT_ATTACK_ARRIVED ) { // Debug_Say(( "Attack Arrvied event %d\n", param )); } } void SoldierObserverClass::Action_Complete( GameObject * obj, int action_id, ActionCompleteReason complete_reason ) { if ( action_id == INNATE_ACTION_ID ) { SubStateString += "\nComplete"; // ActionTimer = MIN( ActionTimer, 10 ); // When he completes, only stay in the action state for 5 more seconds // if I had an enemy target, who was just killed... PhysicalGameObj* enemy = (PhysicalGameObj*)EnemyObject.Get_Ptr(); if ( enemy && State == SOLDIER_AI_ENEMY_SEEN ) { if ( enemy->Get_Defense_Object()->Get_Health() <= 0 ) { // Debug_Say(( "Target dead, moving on\n" )); StateTimer = 100000; // Leave current state EnemyObject = NULL; } } if ( complete_reason == ACTION_COMPLETE_NORMAL || complete_reason == ACTION_COMPLETE_PATH_BAD_START || complete_reason == ACTION_COMPLETE_PATH_BAD_DEST ) { if ( complete_reason == ACTION_COMPLETE_PATH_BAD_DEST ) { // Pick a new action within 1 to 2 seconds ActionTimer = MIN( ActionTimer, WWMath::Random_Float( 1.0F, 2.0F ) ); } else { // Pick a new action within 2 to 3 seconds ActionTimer = MIN( ActionTimer, WWMath::Random_Float( 2.0F, 3.0F ) ); } if ( State > SOLDIER_AI_ALERT_IDLE ) { StateTimer = 100000; // Leave current state } } } } /* ** */ void SoldierObserverClass::State_Changed( SoldierGameObj * soldier ) { switch ( State ) { case SOLDIER_AI_RELAXED_IDLE: IsAlerted = false; break; case SOLDIER_AI_FOOTSTEPS_HEARD: // Leave it was it was break; case SOLDIER_AI_ALERT_IDLE: case SOLDIER_AI_BULLET_HEARD: case SOLDIER_AI_GUNSHOT_HEARD: case SOLDIER_AI_ENEMY_SEEN: IsAlerted = true; break; } // Remeber the last selected weapon if ( soldier->Get_Weapon_Bag()->Get_Index() != 0 ) { LastWeaponIndex = soldier->Get_Weapon_Bag()->Get_Index(); } if ( IsAlerted ) { soldier->Get_Weapon_Bag()->Select_Index( LastWeaponIndex ); } else { soldier->Get_Weapon_Bag()->Select_Index( 0 ); } if ( State == SOLDIER_AI_ALERT_IDLE ) { soldier->Get_Human_State()->Drop_Weapon(); } ActionTimer = 0; } /* ** */ bool SoldierObserverClass::Set_State( SoldierGameObj * soldier, int state, const Vector3& location, GameObject* enemy) { // WWASSERT( soldier != enemy ); LastEvent = state; int old_state = State; // If this is a more extreme state, act surprised! if (StatePriorities[State] < StatePriorities[state]) { // If we have seen the enemy then make a comment. if (state == SOLDIER_AI_ENEMY_SEEN) { // Speak(obj, Pick_Speak_EnemySeen()); } else if (state != SOLDIER_AI_FOOTSTEPS_HEARD) { // Duck // Action_Play_Animation(obj, "s_a_human.h_a_j21c"); // Speak(obj, "Q_Huh02"); } // Debug_Say(("Switching to State %d from %d\n", state, State)); AlertPosition = location; if ( State != state ) { State = state; State_Changed( soldier ); } // Debug_Say(( "New Innate State!\n" )); } // If this is not a less extreme state, reset state timer if (StatePriorities[State] <= StatePriorities[state]) { if ( State != state ) { State = state; State_Changed( soldier ); } StateTimer = 0; if ( ( state == SOLDIER_AI_BULLET_HEARD ) || ( state == SOLDIER_AI_GUNSHOT_HEARD ) ) { if ( ActionTimer > 1 ) { ActionTimer = 1; } } // ActionTimer = 0; no #if 01 // Only take the new pos if closer Vector3 my_pos; soldier->Get_Position(&my_pos); Vector3 old_pos = AlertPosition - my_pos; Vector3 new_pos = location - my_pos; if ( old_pos.Length2() > new_pos.Length2() ) #endif { AlertPosition = location; } if ( enemy != NULL ) { EnemyObject = enemy->As_PhysicalGameObj(); } else { EnemyObject = NULL; } Think(soldier, (State != old_state) ); } // // Reset the conversation timer if the state has changed // if ( old_state != State ) { if ( State == SOLDIER_AI_ENEMY_SEEN ) { ConversationTimer = WWMath::Random_Float( 5.0F, 30.0F ); } else { ConversationTimer = WWMath::Random_Float( 20.0F, 60.0F ); } } // // Decide which (if any) dialogue the soldier should say // if ( ( old_state == SOLDIER_AI_RELAXED_IDLE ) || ( old_state == SOLDIER_AI_ALERT_IDLE ) ) { if ( State == SOLDIER_AI_FOOTSTEPS_HEARD || State == SOLDIER_AI_BULLET_HEARD || State == SOLDIER_AI_GUNSHOT_HEARD) { soldier->Say_Dialogue( DIALOG_STATE_FROM_IDLE_TO_SEARCH ); } else if ( State == SOLDIER_AI_ENEMY_SEEN ) { soldier->Say_Dialogue( DIALOG_STATE_FROM_IDLE_TO_COMBAT ); } } else if ( old_state == SOLDIER_AI_ENEMY_SEEN ) { if ( State == SOLDIER_AI_FOOTSTEPS_HEARD || State == SOLDIER_AI_BULLET_HEARD || State == SOLDIER_AI_GUNSHOT_HEARD) { soldier->Say_Dialogue( DIALOG_STATE_FROM_COMBAT_TO_SEARCH ); } else if ( ( old_state == SOLDIER_AI_RELAXED_IDLE ) || ( old_state == SOLDIER_AI_ALERT_IDLE ) ) { soldier->Say_Dialogue( DIALOG_STATE_FROM_COMBAT_TO_IDLE ); } } else { if ( State == SOLDIER_AI_ENEMY_SEEN ) { soldier->Say_Dialogue( DIALOG_STATE_FROM_SEARCH_TO_COMBAT ); } else if ( ( old_state == SOLDIER_AI_RELAXED_IDLE ) || ( old_state == SOLDIER_AI_ALERT_IDLE ) ) { soldier->Say_Dialogue( DIALOG_STATE_FROM_SEARCH_TO_IDLE ); } } return (old_state != State); } void SoldierObserverClass::Notify_Neighbors_Sound( SoldierGameObj * soldier, const CombatSound & sound ) { // Notify nearby neighbors of my knowledge // for all physicalgameobjs Vector3 my_pos; soldier->Get_Position( &my_pos ); SLNode *objnode; for ( objnode = GameObjManager::Get_Smart_Game_Obj_List()->Head(); objnode; objnode = objnode->Next()) { SmartGameObj *obj = objnode->Data()->As_SmartGameObj(); if ( obj ) { if ( obj == soldier ) continue; if ( obj->Get_Player_Type() != soldier->Get_Player_Type() ) continue; Vector3 obj_pos; obj->Get_Position( &obj_pos ); obj_pos -= my_pos; if ( obj_pos.Length() <= 5 ) { // Notify him of my info const GameObjObserverList & observer_list = obj->Get_Observers(); for( int index = 0; index < observer_list.Count(); index++ ) { observer_list[ index ]->Sound_Heard( obj, sound ); } } } } } void SoldierObserverClass::Notify_Neighbors_Enemy( SoldierGameObj * soldier, GameObject * enemy ) { // Notify nearby neighbors of my knowledge // for all physicalgameobjs Vector3 my_pos; soldier->Get_Position( &my_pos ); SLNode *objnode; for ( objnode = GameObjManager::Get_Smart_Game_Obj_List()->Head(); objnode; objnode = objnode->Next()) { SmartGameObj *obj = objnode->Data()->As_SmartGameObj(); if ( obj ) { if ( obj == soldier ) continue; if ( obj->Get_Player_Type() != soldier->Get_Player_Type() ) continue; Vector3 obj_pos; obj->Get_Position( &obj_pos ); obj_pos -= my_pos; if ( obj_pos.Length() <= 5 ) { // Notify him of my info const GameObjObserverList & observer_list = obj->Get_Observers(); for( int index = 0; index < observer_list.Count(); index++ ) { observer_list[ index ]->Enemy_Seen( obj, enemy ); } } } } } void SoldierObserverClass::Poked( GameObject * obj, GameObject * poker ) { if ( obj != NULL && obj->As_PhysicalGameObj() != NULL ) { PhysicalGameObj *physical_obj = obj->As_PhysicalGameObj(); if ( physical_obj->As_SoldierGameObj() != NULL ) { bool face_poker = false; // // Get the soldier to say one of its pre-canned remarks // SoldierGameObj *soldier = physical_obj->As_SoldierGameObj(); float duration = 2.0F; switch ( State ) { case SOLDIER_AI_RELAXED_IDLE: case SOLDIER_AI_ALERT_IDLE: soldier->Say_Dialogue( DIALOG_ON_POKE_IDLE ); face_poker = true; break; case SOLDIER_AI_FOOTSTEPS_HEARD: case SOLDIER_AI_BULLET_HEARD: case SOLDIER_AI_GUNSHOT_HEARD: soldier->Say_Dialogue( DIALOG_ON_POKE_SEARCH ); face_poker = true; break; case SOLDIER_AI_ENEMY_SEEN: soldier->Say_Dialogue( DIALOG_ON_POKE_COMBAT ); break; } // // Increment the duration by a small random amount // duration += WWMath::Random_Float( 0.5F, 2.0F ); PhysicalGameObj *physical_poker_obj = poker->As_PhysicalGameObj(); if ( physical_poker_obj != NULL ) { // // Get the position of the player who poked us // Vector3 poker_pos = physical_poker_obj->Get_Bullseye_Position(); // // Make the soldier turn his body to face the speaker // if ( face_poker ) { ActionParamsStruct parameters; parameters.Priority = 20; parameters.ObserverID = Get_ID(); parameters.Set_Face_Location( poker_pos, duration ); soldier->Get_Action()->Face_Location( parameters ); } // // Turn the soldier's head to look at the speaker // soldier->Look_At( poker_pos, duration ); } } } return ; } void SoldierObserverClass::Think( SoldierGameObj * soldier, bool is_new_state ) { WWPROFILE( "SoldierObserver Think" ); WWASSERT( soldier != NULL ); // // We can't switch states if the soldier is "busy". Busy usually // means he's going up an elevator, climbing a ladder, or opening a door. // if ( soldier->Get_Action()->Is_Busy() ) { return ; } // Check how long we have been in this state, and if we should switch StateTimer += 1; if (StateTimer >= StateData[State].StateDuration) { // Debug_Say(("Soldier relaxing from state %d to %d\n", State, StateData[State].NextState)); Action_Reset( soldier ); // reset the old state State = StateData[State].NextState; if ( State == SOLDIER_AI_CONDITIONAL_IDLE ) { State = ( IsAlerted ) ? SOLDIER_AI_ALERT_IDLE : SOLDIER_AI_RELAXED_IDLE; } State_Changed( soldier ); StateTimer = 0; } if ( ( State != SOLDIER_AI_RELAXED_IDLE ) && ( State != SOLDIER_AI_ALERT_IDLE ) ) { soldier->Reset_Loiter_Delay(); } // Check if time for another state action if (soldier->Is_Innate_Enabled(SOLDIER_INNATE_ACTIONS)) { ActionTimer -= 1; if (ActionTimer <= 0) { State_Act(soldier, is_new_state); } } // // Allow this unit to start a conversation regardless of the state he's in // if ( soldier->Is_Human_Controlled () == false && soldier->Is_In_Conversation () == false ) { // // Is it time to start a new conversation? // ConversationTimer -= 1.0F; if (ConversationTimer <= 0) { // // Don't start an innate conversation if there are already // conversations taking place // #ifdef ENABLE_INNATE_CONVERSATIONS if ( ConversationMgrClass::Get_Active_Conversation_Count () == 0 ) { ConversationMgrClass::Start_Conversation( soldier ); } #endif //ENABLE_INNATE_CONVERSATIONS Reset_Conversation_Timer(); } } } void SoldierObserverClass::Get_Information( StringClass & string ) { StringClass temp; string += "-----------\n"; temp.Format( "State: %s\n", SoldierAIStateNames[ State ] ); string += temp; temp.Format( "Sub: %s\n", SubStateString ); string += temp; if ( IsStationary ) { string += "Stationary\n"; } if ( State == SOLDIER_AI_ENEMY_SEEN ) { temp.Format( "Attacking: %d\n", EnemyObject.Get_Ptr() ? EnemyObject.Get_Ptr()->Get_ID() : 0 ); string += temp; } temp.Format( "Last: %s\n", SoldierAIStateNames[ LastEvent ] ); string += temp; temp.Format( "Act Time: %f\n", ActionTimer ); string += temp; if ( CoverPosition != NULL ) { temp.Format( "Has Cover Pos\n" ); string += temp; } } void SoldierObserverClass::Reset_Conversation_Timer( void ) { ConversationTimer = WWMath::Random_Float( 20.0F, 60.0F ); } /*********************************************************************************************** ** Innate AI State Actions ***********************************************************************************************/ void SoldierObserverClass::State_Act( SoldierGameObj * soldier, bool is_new_state ) { WWASSERT( soldier != NULL ); switch (State) { case SOLDIER_AI_RELAXED_IDLE: State_Act_Idle( soldier ); break; case SOLDIER_AI_FOOTSTEPS_HEARD: State_Act_Footsteps_Heard( soldier ); break; case SOLDIER_AI_ALERT_IDLE: case SOLDIER_AI_BULLET_HEARD: State_Act_Bullet_Heard( soldier, is_new_state ); break; case SOLDIER_AI_GUNSHOT_HEARD: State_Act_Gunshot_Heard( soldier ); break; case SOLDIER_AI_ENEMY_SEEN: State_Act_Attack( soldier ); break; } } #define IDLE_WALK_DISTANCE 6 #define IDLE_WALK_SPEED 0.3f // was 0.2f #define ALERTED_RUN_DISTANCE 6 #define ALERTED_RUN_SPEED 0.9f #define ALERTED_RUN_RANGE 2 /* ** */ void SoldierObserverClass::State_Act_Idle( SoldierGameObj * soldier ) { ActionTimer = 5; // Will probably be overridden Vector3 position; soldier->Get_Position(&position); Release_Cover_Position(); if ( FreeRandom.Get_Float() < 0.5f ) { // 50% chance of facing a random direction SubStateString = "Facing Random"; position += Random_Vector(1); if ( COMBAT_STAR ) { SubStateString = "Facing Star"; COMBAT_STAR->Get_Position( &position ); } Action_Face_Location( soldier, position, AI_STATE_IDLE ); ActionTimer = FreeRandom.Get_Float(4, 6); //Delay 4 to 6 seconds } else if ( !IsStationary ) { // 50% chance of walking to a random point SubStateString = "Random Walk"; PathfindClass *pathfind = PathfindClass::Get_Instance(); // Lookup a safe random position to walk to if ( pathfind->Find_Random_Spot( position, IDLE_WALK_DISTANCE, &position ) ) { Action_Goto_Location( soldier, position, AI_STATE_IDLE, IDLE_WALK_SPEED, 0.3f ); } ActionTimer = FreeRandom.Get_Float(7, 10); //Delay 7 to 10 seconds } Look_Random( soldier, ActionTimer ); } /* ** */ void SoldierObserverClass::State_Act_Footsteps_Heard( SoldierGameObj * soldier ) { ActionTimer = FreeRandom.Get_Float(3, 4); //Delay 7 to 10 seconds Release_Cover_Position(); bool walking = false; if ( FreeRandom.Get_Float() < Aggressiveness && !IsStationary ) { SubStateString = "Walk To Footsteps"; Vector3 position; PathfindClass *pathfind = PathfindClass::Get_Instance(); if ( pathfind->Find_Random_Spot( AlertPosition, 2, &position ) ) { Action_Goto_Location( soldier, position, AI_STATE_IDLE, IDLE_WALK_SPEED, 0.5f ); walking = true; } } if ( !walking ) { SubStateString = "Face Footsteps"; Action_Face_Location( soldier, AlertPosition, AI_STATE_SEARCH ); } Look_Random( soldier, 0 ); // No random looking! } /* ** */ void SoldierObserverClass::State_Act_Bullet_Heard( SoldierGameObj * soldier, bool is_new_state ) { ActionTimer = FreeRandom.Get_Float(5,10); ActionTimer = FreeRandom.Get_Float(2,4); soldier->Get_Human_State()->Raise_Weapon(); // Keep the gun up Vector3 current_position; soldier->Get_Position(¤t_position); Vector3 walk_position; #define RANGE 10 // Dive with probability tied to bullet distance; 100% at 10m, 0 percent at 20m float range = (current_position - AlertPosition).Length(); float percent = WWMath::Clamp( (1 - ((range-RANGE)/RANGE)), 0, 1 ); bool dive = is_new_state && (FreeRandom.Get_Float() < percent) && !IsStationary; if ( dive ) { SubStateString = "Dive Away"; // Dive away ActionTimer = FreeRandom.Get_Float(2,4); Vector3 other_dir = 2*current_position - AlertPosition; Action_Dive( soldier, other_dir ); } else { bool done = false; if ( !IsStationary ) { done = Take_Cover( soldier ); } if ( !done ) { if ( FreeRandom.Get_Float() < TakeCoverProbability || IsStationary ) { SubStateString = "Face Random Crouched"; // Face and crouch walk_position = current_position + Random_Vector(1); if ( COMBAT_STAR ) { SubStateString = "Face Star Crouched"; COMBAT_STAR->Get_Position( &walk_position ); } Action_Face_Location( soldier, walk_position, AI_STATE_IDLE, true ); } else { SubStateString = "Run To"; // Lookup a safe random position to run to PathfindClass *pathfind = PathfindClass::Get_Instance(); if ( pathfind->Find_Random_Spot( AlertPosition, ALERTED_RUN_DISTANCE, &walk_position ) ) { Action_Goto_Location( soldier, walk_position, AI_STATE_SEARCH, ALERTED_RUN_SPEED, ALERTED_RUN_RANGE, true ); } } } } Look_Random( soldier, ActionTimer ); } /* ** */ void SoldierObserverClass::State_Act_Gunshot_Heard( SoldierGameObj * soldier ) { soldier->Get_Human_State()->Raise_Weapon(); // Keep the gun up Look_Random( soldier, 0 ); ActionTimer = FreeRandom.Get_Float( 5, 10 ); if ( !IsStationary ) { bool done = Take_Cover( soldier, true, AlertPosition ); if ( !done ) { SubStateString = "Run To Random"; PathfindClass *pathfind = PathfindClass::Get_Instance(); // // Lookup a safe random position to run to // Vector3 position = AlertPosition; if (pathfind->Find_Random_Spot( AlertPosition, ALERTED_RUN_DISTANCE, &position )) { Action_Goto_Location_Facing(soldier, position, AI_STATE_SEARCH, AlertPosition, ALERTED_RUN_SPEED, ALERTED_RUN_RANGE); } else { // // Wasn't possible to run to a random location near the alert position, // so simply move around your local area a little bit and look at the alert position // Vector3 curr_pos; soldier->Get_Position (&curr_pos); if (pathfind->Find_Random_Spot( curr_pos, IDLE_WALK_DISTANCE, &position )) { Action_Goto_Location_Facing( soldier, position, AI_STATE_SEARCH, AlertPosition, IDLE_WALK_SPEED, 0.5F ); } //Release_Cover_Position(); //SubStateString = "Face gunshot"; //Action_Face_Location( soldier, AlertPosition, AI_STATE_SEARCH ); } } } else { Release_Cover_Position(); SubStateString = "Face gunshot"; Action_Face_Location( soldier, AlertPosition, AI_STATE_SEARCH ); } } /* ** */ void SoldierObserverClass::State_Act_Attack( SoldierGameObj * soldier ) { WWASSERT( soldier != NULL ); soldier->Get_Human_State()->Raise_Weapon(); // Keep the gun up ActionTimer = FreeRandom.Get_Float( 5, 8 ); Look_Random( soldier, 0 ); #define RUN_SPEED 0.9f #define WALK_SPEED 0.2f bool done = false; // OK, here's the attack plan.... // Periodically (every 5 seconds?), pick a new attack action. 4 Attack options are: // If in a attack point, jump back to the cover // If in a cover spot, possibily jump out and attack (goto attack point) // If there is a new cover spot available, goto it // Pick a random point and goto it PhysicalGameObj* enemy = (PhysicalGameObj*)EnemyObject.Get_Ptr(); // WWASSERT( soldier != enemy ); if ( enemy == NULL ) { StateTimer = 100000; // Leave this state return; } Vector3 enemy_pos; enemy->Get_Position(&enemy_pos); float weapon_range = 60.0f; if ( soldier->Get_Weapon() ) { weapon_range = soldier->Get_Weapon()->Get_Range(); } Vector3 current_position; soldier->Get_Position(¤t_position); if ( CoverPosition != NULL ) { if ( CoveredAttack == true ) { SubStateString = "Return to Cover"; Vector3 cover_position = CoverPosition->Get_Transform().Get_Translation(); Action_Attack_Object( soldier, enemy, weapon_range, CoverPosition->Get_Crouch(), cover_position, 0.5f ); CoveredAttack = false; ActionTimer = 5; // for 3 seconds done = true; } else if ( FreeRandom.Get_Float() < Aggressiveness ) { if ( soldier->Get_Weapon() != NULL ) { /* ** Start a covered attack. ** Pick an attack point, and go there, attacking at the target */ SubStateString = "Covered Attack"; Vector3 attack_point = CoverPosition->Get_Attack_Position( enemy_pos ); Action_Attack_Object( soldier, enemy, weapon_range, false, attack_point, 0.5f ); CoveredAttack = true; ActionTimer = 5; // for 3 seconds done = true; } } } if ( CoverPosition != NULL ) { if ( !CoverManager::Is_Cover_Safe( CoverPosition, enemy_pos ) ) { Release_Cover_Position(); CoveredAttack = false; done = false; } } float effective_range = 20; if ( soldier->Get_Weapon() ) { effective_range = soldier->Get_Weapon()->Get_Effective_Range(); } if ( !done && ( FreeRandom.Get_Float() < TakeCoverProbability ) && !IsStationary ) { // 25% chance of not finding cover Release_Cover_Position(); // Give us the option of selecting our current spot CoverEntryClass * cover = CoverManager::Request_Cover(current_position, enemy_pos, effective_range); if (cover != NULL) { // Yes, take it SubStateString = "Take Cover"; CoverPosition = cover; Vector3 cover_position = cover->Get_Transform().Get_Translation(); Action_Attack_Object( soldier, enemy, weapon_range, CoverPosition->Get_Crouch(), cover_position, 0.5f ); ActionTimer = 15; done = true; } } if ( FreeRandom.Get_Float() < Aggressiveness && !IsStationary ) { // Charge Attack Vector3 pos; enemy->Get_Position( &pos ); SubStateString = "Charge Attack"; ActionTimer = 10; bool kneel = ( FreeRandom.Get_Float() < (1-Aggressiveness) ); Action_Attack_Object( soldier, enemy, weapon_range, kneel, pos, 5 ); done = true; } if ( !done ) { Vector3 best_pos = current_position; float best_distance = 100000; for (int i = 0; i < 3; i++) { // Lookup a safe random position to run to Vector3 pos = current_position; PathfindClass *pathfind = PathfindClass::Get_Instance(); if ( pathfind->Find_Random_Spot( current_position, effective_range*2, &pos ) ) { float distance = (pos - enemy_pos).Length(); if (WWMath::Fabs(distance - effective_range) < best_distance) { best_distance = WWMath::Fabs(distance - effective_range); best_pos = pos; } } } if ( IsStationary ) { best_pos = current_position; } SubStateString = "Random Run Attack"; ActionTimer = 10; bool kneel = ( FreeRandom.Get_Float() < (1-Aggressiveness) ); Action_Attack_Object( soldier, enemy, weapon_range, kneel, best_pos, 0.5f ); } } /*********************************************************************************************** ** Innate AI Actions ***********************************************************************************************/ void SoldierObserverClass::Action_Reset( SoldierGameObj * soldier ) { WWASSERT( soldier != NULL ); soldier->Get_Action()->Reset( StatePriorities[ State ] ); } void SoldierObserverClass::Action_Face_Location( SoldierGameObj * soldier, const Vector3 & location, SoldierAIState ai_state, bool crouched ) { WWASSERT( soldier != NULL ); ActionParamsStruct parameters; parameters.Set_Basic( Get_ID(), StatePriorities[ State ], INNATE_ACTION_ID, ai_state ); parameters.Set_Face_Location( location, 2 ); parameters.MoveCrouched = crouched; soldier->Get_Action()->Face_Location( parameters ); } void SoldierObserverClass::Action_Goto_Location( SoldierGameObj * soldier, const Vector3 & location, SoldierAIState ai_state, float speed, float distance, bool crouched ) { WWASSERT( soldier != NULL ); Vector3 move_position = location; Stay_Within_Home( soldier, &move_position, &distance ); ActionParamsStruct parameters; parameters.Set_Basic( Get_ID(), StatePriorities[ State ], INNATE_ACTION_ID, ai_state ); parameters.Set_Movement( move_position, speed, distance, crouched ); soldier->Get_Action()->Goto( parameters ); } void SoldierObserverClass::Action_Goto_Location_Facing( SoldierGameObj * soldier, const Vector3 & location, SoldierAIState ai_state, const Vector3 & facing_pos, float speed, float distance, bool crouched ) { WWASSERT( soldier != NULL ); Vector3 move_position = location; Stay_Within_Home( soldier, &move_position, &distance ); ActionParamsStruct parameters; parameters.Set_Basic( Get_ID(), StatePriorities[ State ], INNATE_ACTION_ID, ai_state ); parameters.Set_Movement( move_position, speed, distance, crouched ); parameters.ForceFacing = true; parameters.FaceLocation = facing_pos; soldier->Get_Action()->Goto( parameters ); } void SoldierObserverClass::Action_Attack_Object( SoldierGameObj * soldier, PhysicalGameObj * enemy, float range, bool kneel, const Vector3 & move_location, float arrived_distance ) { WWASSERT( soldier != NULL ); Vector3 move_position = move_location; Stay_Within_Home( soldier, &move_position, &arrived_distance ); if ( IsStationary ) { // Don't move if stationary arrived_distance = 10000; } if ( enemy != NULL && soldier->Get_Weapon() != NULL ) { ActionParamsStruct parameters; parameters.Set_Basic( Get_ID(), StatePriorities[ State ], INNATE_ACTION_ID, AI_STATE_COMBAT ); parameters.Set_Movement( move_position, RUN_SPEED, arrived_distance, kneel ); parameters.Set_Attack( enemy, range, FreeRandom.Get_Float(2), true ); parameters.AttackCrouched = kneel; parameters.AttackWanderAllowed = !IsStationary; // 50% chance of looking where you are running, for a bit if ( FreeRandom.Get_Float() < 0.5 ) { parameters.LookLocation = move_position + Vector3( 0,0,1 ); parameters.LookDuration = FreeRandom.Get_Float( 0.3f, 1 ); } soldier->Get_Action()->Attack( parameters ); } } void SoldierObserverClass::Action_Dive( SoldierGameObj * soldier, const Vector3 & location ) { WWASSERT( soldier != NULL ); ActionParamsStruct parameters; parameters.Set_Basic( Get_ID(), StatePriorities[ State ], INNATE_ACTION_ID, AI_STATE_SEARCH ); parameters.MoveLocation = location; soldier->Get_Action()->Dive( parameters ); } void SoldierObserverClass::Stay_Within_Home( SoldierGameObj * soldier, Vector3 * location, float * distance ) { // Whenever we move, if we are outside our home radius, override the move to move back inside Vector3 home_diff; soldier->Get_Position( &home_diff ); home_diff -= HomeLocation; home_diff.Z = 0; // don't consider Z if ( home_diff.Length() > HomeRadius ) { *location = HomeLocation; *distance = HomeRadius; } } bool SoldierObserverClass::Take_Cover( SoldierGameObj * soldier, bool force_face, const Vector3 & face_pos ) { if ( CoverPosition != NULL ) { if ( !CoverManager::Is_Cover_Safe( CoverPosition, AlertPosition ) ) { Release_Cover_Position(); } } // Possibily take a cover spot if ( FreeRandom.Get_Float() < TakeCoverProbability ) { Release_Cover_Position(); // Give us the option of selecting our current spot float cover_range = 20; Vector3 current_position; soldier->Get_Position(¤t_position); CoverEntryClass * cover = CoverManager::Request_Cover(current_position, AlertPosition, cover_range); // Can we find a cover spot? if (cover != NULL) { // Yes, take it SubStateString = "Take Cover"; CoverPosition = cover; Vector3 cover_position = cover->Get_Transform().Get_Translation(); if ( force_face ) { Action_Goto_Location_Facing(soldier, cover_position, AI_STATE_SEARCH, face_pos, ALERTED_RUN_SPEED, 0.5, cover->Get_Crouch() ); } else { Action_Goto_Location(soldier, cover_position, AI_STATE_SEARCH, ALERTED_RUN_SPEED, 0.5, cover->Get_Crouch() ); } return true; } } return false; } void SoldierObserverClass::Look_Random( SoldierGameObj * soldier, float duration ) { soldier->Look_Random( duration ); }