/* ** Command & Conquer Generals(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 . */ //////////////////////////////////////////////////////////////////////////////// // // // (c) 2001-2003 Electronic Arts Inc. // // // //////////////////////////////////////////////////////////////////////////////// // AssaultTransportAIUpdate.cpp //////////// // Author: Kris Morness, December 2002 // Desc: State machine that allows assault transports (troop crawler) to deploy // troops, order them to attack, then return. Can do extra things like ordering // injured troops to return to the transport for healing purposes. #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "Common/Player.h" #include "Common/ThingFactory.h" #include "GameClient/Drawable.h" #include "GameClient/InGameUI.h" #include "GameLogic/ExperienceTracker.h" #include "GameLogic/Module/BodyModule.h" #include "GameLogic/Module/ContainModule.h" #include "GameLogic/Module/AssaultTransportAIUpdate.h" #include "GameLogic/Module/PhysicsUpdate.h" #include "GameLogic/Object.h" #include "GameLogic/PartitionManager.h" #include "GameLogic/Weapon.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- AssaultTransportAIUpdate::AssaultTransportAIUpdate( Thing *thing, const ModuleData* moduleData ) : AIUpdateInterface( thing, moduleData ) { m_currentMembers = MAX_TRANSPORT_SLOTS; //First time, max it out, to ensure clearing arrays in reset. reset(); } //------------------------------------------------------------------------------------------------- void AssaultTransportAIUpdate::reset() { for( int i = 0; i < m_currentMembers; i++ ) { m_memberIDs[ i ] = INVALID_ID; m_memberHealing[ i ] = FALSE; m_newMember[ i ] = FALSE; } m_currentMembers = 0; m_attackMoveGoalPos.zero(); m_designatedTarget = INVALID_ID; m_state = IDLE; m_framesRemaining = 0; m_isAttackMove = FALSE; m_isAttackObject = FALSE; m_newOccupantsAreNewMembers = FALSE; } //------------------------------------------------------------------------------------------------- AssaultTransportAIUpdate::~AssaultTransportAIUpdate( void ) { } //------------------------------------------------------------------------------------------------- void AssaultTransportAIUpdate::aiDoCommand(const AICommandParms* parms) { //Inspect the command and reset everything when necessary. if( parms->m_cmdSource != CMD_FROM_AI ) { //Now the only time we care about anything is if we were ordered to attack something or attack move. switch( parms->m_cmd ) { case AICMD_ATTACKMOVE_TO_POSITION: //Reset because we have been ordered to do something. reset(); m_attackMoveGoalPos = parms->m_pos; m_isAttackMove = TRUE; break; case AICMD_ATTACK_OBJECT: //Reset because we have been ordered to do something. reset(); //m_designatedTarget = parms->m_obj ? parms->m_obj->getID() : INVALID_ID; m_isAttackObject = TRUE; break; case AICMD_IDLE: m_designatedTarget = INVALID_ID; //Order all outside members back inside! retrieveMembers(); reset(); break; default: //Reset because we have been ordered to do something we're not handling. reset(); break; } } //Note, in both cases, the transport will fire a dummy DEPLOY weapon that will trigger the //evacuation of the troops. AIUpdateInterface::aiDoCommand( parms ); } //------------------------------------------------------------------------------------------------- void AssaultTransportAIUpdate::beginAssault( const Object *designatedTarget ) const { //The transport has determined it is in range to begin the assault (via weapon system). //Now order the evacuation of healthy troops, and let the update handle moving them. if( designatedTarget ) { m_designatedTarget = designatedTarget->getID(); } } //------------------------------------------------------------------------------------------------- Bool AssaultTransportAIUpdate::isIdle() const { return AIUpdateInterface::isIdle(); } //------------------------------------------------------------------------------------------------- UpdateSleepTime calcSleepTime() { return UPDATE_SLEEP_NONE; } //------------------------------------------------------------------------------------------------- UpdateSleepTime AssaultTransportAIUpdate::update( void ) { Object *transport = getObject(); //const AssaultTransportAIUpdateModuleData *data = getAssaultTransportAIUpdateModuleData(); if( transport->isEffectivelyDead() ) { giveFinalOrders(); return UPDATE_SLEEP_FOREVER; } //First removing dead members or members that have been ordered to do something outside of this AI. if( m_currentMembers ) { for( int i = 0; i < m_currentMembers; i++ ) { Object *member = TheGameLogic->findObjectByID( m_memberIDs[ i ] ); AIUpdateInterface *ai = member ? member->getAI() : NULL; if( !member || member->isEffectivelyDead() || ai->getLastCommandSource() != CMD_FROM_AI ) { //Member is toast -- so remove him from our list! if( m_currentMembers - 1 > i ) { //Move the last slot to this slot to keep array contiguous. m_memberIDs[ i ] = m_memberIDs[ m_currentMembers - 1 ]; m_memberHealing[ i ] = m_memberHealing[ m_currentMembers - 1 ]; m_newMember[ i ] = m_newMember[ m_currentMembers - 1 ]; } else { //Just clean out last slot. m_memberIDs[ i ] = INVALID_ID; m_memberHealing[ i ] = FALSE; m_newMember[ i ] = FALSE; } if( ai ) { //Important! Members of our assault transport must be allowed to chase down designated enemies. //Generally only player commands allow this, so this flag allows AI commands to do the same. //We need to turn this off though, because this ex-member is no longer under transport control. ai->setAllowedToChase( FALSE ); } m_currentMembers--; } } } //Now add any potentially new members to the group. ContainModuleInterface *contain = transport->getContain(); if( contain ) { const ContainedItemsList *passengerList = contain->getContainedItemsList(); ContainedItemsList::const_iterator passengerIterator; passengerIterator = passengerList->begin(); while( passengerIterator != passengerList->end() ) { Object *passenger = *passengerIterator; //Advance to the next iterator passengerIterator++; //Make sure it isn't in our list already. Bool found = FALSE; for( int i = 0; i < m_currentMembers; i++ ) { if( passenger->getID() == m_memberIDs[ i ] ) { //He is in the list... so skip him. found = TRUE; break; } } if( found ) { //Get next passenger. continue; } //It's possible to add members manually -- but if we already have 10 members, then wait! if( m_currentMembers < MAX_TRANSPORT_SLOTS ) { //Not in list, so add him! m_memberIDs[ m_currentMembers ] = passenger->getID(); if( passenger->getAI() ) { //Important! Members of our assault transport must be allowed to chase down designated enemies. //Generally only player commands allow this, so this flag allows AI commands to do the same. passenger->getAI()->setAllowedToChase( TRUE ); } //Check if the passenger is wounded below threshhold (if so make sure we heal him before ordering him to fight!) if( isMemberWounded( passenger ) ) { m_memberHealing[ m_currentMembers ] = TRUE; } if( m_newOccupantsAreNewMembers ) { //New members won't eject out until a new attack order is issued. m_newMember[ m_currentMembers ] = TRUE; } m_currentMembers++; } } m_newOccupantsAreNewMembers = TRUE; } if( isAttackPointless() ) { aiIdle( CMD_FROM_AI ); return UPDATE_SLEEP_NONE; } //Keep track of the average position of all combat units assigned to me. Coord3D fighterCentroidPos; UnsignedInt fightingMembers = 0; fighterCentroidPos.zero(); //If we're already in the process, reacquire the designated target again... see if //it's still alive. Object *designatedTarget = TheGameLogic->findObjectByID( m_designatedTarget ); if( designatedTarget && designatedTarget->isEffectivelyDead() ) { designatedTarget = NULL; } if( designatedTarget ) { //Look for members not currently attacking this target. for( int i = 0; i < m_currentMembers; i++ ) { Object *member = TheGameLogic->findObjectByID( m_memberIDs[ i ] ); AIUpdateInterface *ai = member ? member->getAI() : NULL; if( member && ai ) { Bool contained = member->isContained(); Bool wounded = isMemberWounded( member ); if( contained && isMemberHealthy( member ) && !m_newMember[ i ] ) { //This contained member is healthy so order him to exit to start fighting! //New members are exempt! ai->aiExit( transport, CMD_FROM_AI ); } if( !contained ) { if( wounded ) { if( ai->getAIStateType() != AI_ENTER ) { //Order wounded members back to get healed. ai->aiEnter( transport, CMD_FROM_AI ); } } else { //Increment the number of fighters and their position. fighterCentroidPos.add( member->getPosition() ); fightingMembers++; if( !ai->isMoving() ) { if( ai->getGoalObject() != designatedTarget ) { //Okay, this dude is outside and waiting... order him to attack the designated target ai->aiAttackObject( designatedTarget, NO_MAX_SHOTS_LIMIT, CMD_FROM_AI ); } } } } } } } else { if( m_isAttackMove && getAIStateType() != AI_ATTACK_MOVE_TO ) { //Continue to move towards the attackmove area. aiAttackMoveToPosition( &m_attackMoveGoalPos, NO_MAX_SHOTS_LIMIT, CMD_FROM_AI ); } else if( m_isAttackObject ) { retrieveMembers(); } } /* //Keep near the troops. if( !m_framesRemaining ) { if( !isMoving() && fightingMembers && designatedTarget ) { m_framesRemaining = 45; //Get centriod pos now that we know the number of fighting members. Real scale = 1.0f / (Real)fightingMembers; fighterCentroidPos.scale( scale ); Coord3D designatedTargetPos = *designatedTarget->getPosition(); //Calculate a vector from the target passed the fighters to be at a safe place //to be as a transport. Coord3D vector; vector.set( &fighterCentroidPos ); vector.sub( &designatedTargetPos ); vector.normalize(); vector.scale( 150.0f ); Coord3D transportGoalPos; transportGoalPos.set( &designatedTargetPos ); transportGoalPos.add( &vector ); Real distanceSqrd = ThePartitionManager->getDistanceSquared( transport, &transportGoalPos, FROM_CENTER_2D ); if( distanceSqrd > 40.0f * 40.0f ) { //Order the transport to move to the safer position //aiMoveToPosition( &transportGoalPos, CMD_FROM_AI ); } } } else { m_framesRemaining--; } if( designatedTarget && !isMoving() ) { //Order the transport to face the designated target! //aiFaceObject( designatedTarget, CMD_FROM_AI ); } */ /*UpdateSleepTime ret =*/ AIUpdateInterface::update(); //return (mine < ret) ? mine : ret; /// @todo srj -- someday, make sleepy. for now, must not sleep. return UPDATE_SLEEP_NONE; } //------------------------------------------------------------------------------------------------- Bool AssaultTransportAIUpdate::isAttackPointless() const { //If all members are new members (thus can't attack), and the transport itself //is still attacking, stop! const Object *transport = getObject(); if( transport->testStatus( OBJECT_STATUS_IS_ATTACKING ) ) { for( int i = 0; i < m_currentMembers; i++ ) { if( !m_newMember[ i ] ) { //We have a non-new member, so attack is valid. return FALSE; } } //We are trying to attack, but can't because all our members are new. return TRUE; } //We aren't trying to attack, so everything is good. return FALSE; } //------------------------------------------------------------------------------------------------- Bool AssaultTransportAIUpdate::isMemberWounded( const Object *member ) const { const AssaultTransportAIUpdateModuleData *data = getAssaultTransportAIUpdateModuleData(); BodyModuleInterface *body = member->getBodyModule(); if( body ) { Real ratio = body->getHealth() / body->getMaxHealth(); if( ratio < data->m_membersGetHealedAtLifeRatio ) { return TRUE; } } return FALSE; } //------------------------------------------------------------------------------------------------- Bool AssaultTransportAIUpdate::isMemberHealthy( const Object *member ) const { BodyModuleInterface *body = member->getBodyModule(); if( body ) { if( body->getHealth() == body->getMaxHealth() ) { return TRUE; } } return FALSE; } //------------------------------------------------------------------------------------------------- void AssaultTransportAIUpdate::retrieveMembers() { //Order all outside members back inside! for( int i = 0; i < m_currentMembers; i++ ) { Object *member = TheGameLogic->findObjectByID( m_memberIDs[ i ] ); AIUpdateInterface *ai = member ? member->getAI() : NULL; if( member && ai ) { Bool contained = member->isContained(); if( !contained ) { //This contained member is healthy so order him to exit to start fighting! ai->aiEnter( getObject(), CMD_FROM_AI ); } } } } //------------------------------------------------------------------------------------------------- void AssaultTransportAIUpdate::giveFinalOrders() { //All members have been ejected outside already -- transfer the original order to the troops for( int i = 0; i < m_currentMembers; i++ ) { Object *member = TheGameLogic->findObjectByID( m_memberIDs[ i ] ); AIUpdateInterface *ai = member ? member->getAI() : NULL; if( member && ai ) { Object *designatedTarget = TheGameLogic->findObjectByID( m_designatedTarget ); if( m_isAttackObject && designatedTarget ) { ai->aiAttackObject( designatedTarget, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); } else if( m_isAttackMove ) { ai->aiAttackMoveToPosition( &m_attackMoveGoalPos, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); } ai->setAllowedToChase( FALSE ); } } } //------------------------------------------------------------------------------------------------- /** CRC */ //------------------------------------------------------------------------------------------------- void AssaultTransportAIUpdate::crc( Xfer *xfer ) { // extend base class AIUpdateInterface::crc(xfer); } // end crc //------------------------------------------------------------------------------------------------- /** Xfer method * Version Info: * 1: Initial version */ //------------------------------------------------------------------------------------------------- void AssaultTransportAIUpdate::xfer( Xfer *xfer ) { // version XferVersion currentVersion = 1; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); // extend base class AIUpdateInterface::xfer(xfer); xfer->xferInt( &m_currentMembers ); for( int i = 0; i < m_currentMembers; i++ ) { xfer->xferObjectID( &(m_memberIDs[ i ]) ); xfer->xferBool( &(m_memberHealing[ i ]) ); } xfer->xferCoord3D( &m_attackMoveGoalPos ); xfer->xferObjectID( &m_designatedTarget ); Int state = (Int)m_state; xfer->xferInt( &state ); m_state = (AssaultStateTypes)state; xfer->xferUnsignedInt( &m_framesRemaining ); xfer->xferBool( &m_isAttackMove ); xfer->xferBool( &m_isAttackObject ); } // end xfer //------------------------------------------------------------------------------------------------- /** Load post process */ //------------------------------------------------------------------------------------------------- void AssaultTransportAIUpdate::loadPostProcess( void ) { // extend base class AIUpdateInterface::loadPostProcess(); } // end loadPostProcess