Files
source/TacticalAI/CreatureDecideAction.cpp
Asdow 52a4959dfb Add optional InOut parameter to ClosestKnownOpponent
distanceInCellCoords will return the distance between pSoldier and closest known opponent. We're often calculating same range after the call using PythSpacesAway and this way it's no longer necessary in most cases.
2026-01-19 22:56:18 +02:00

1676 lines
52 KiB
C++

#include "types.h"
//#include "Soldier Control.h"
#include "ai.h"
#include "AIInternals.h"
#include "opplist.h"
#include "Items.h"
#include "Rotting Corpses.h"
#include "Soldier Add.h"
//forward declarations of common classes to eliminate includes
class OBJECTTYPE;
class SOLDIERTYPE;
extern STR8 gStr8AlertStatus[];
extern STR8 gStr8Attitude[];
extern STR8 gStr8Orders[];
extern STR8 gStr8Team[];
extern STR8 gStr8Class[];
extern INT8 STRAIGHT; //lal
#define CAN_CALL( s ) (s->ubBodyType != BLOODCAT && s->ubBodyType != LARVAE_MONSTER && s->ubBodyType != INFANT_MONSTER)
#define CAN_LISTEN_TO_CALL( s ) (s->ubBodyType != BLOODCAT && s->ubBodyType != LARVAE_MONSTER)
enum
{
CALLER_FEMALE = 0,
CALLER_MALE,
CALLER_INFANT,
CALLER_QUEEN,
NUM_CREATURE_CALLERS
} CreatureCaller;
enum
{
CREATURE_MOBILE = 0,
CREATURE_CRAWLER,
CREATURE_IMMOBILE
} CreatureMobility;
#define FRENZY_THRESHOLD 8
#define MAX_EAT_DIST 5
INT8 gbCallPriority[NUM_CREATURE_CALLS][NUM_CREATURE_CALLERS] =
{
{0, 0, 0 },//CALL_NONE
{3, 5, 12},//CALL_1_PREY
{5, 9, 12},//CALL_MULTIPLE_PREY
{4, 7, 12},//CALL_ATTACKED
{6, 9, 12},//CALL_CRIPPLED
};
INT8 gbHuntCallPriority[NUM_CREATURE_CALLS] =
{
4, //CALL_1_PREY
5, //CALL_MULTIPLE_PREY
7, //CALL_ATTACKED
8 //CALL_CRIPPLED
};
#define PRIORITY_DECR_DISTANCE 30
#define CALL_1_OPPONENT CALL_1_PREY
#define CALL_MULTIPLE_OPPONENT CALL_MULTIPLE_PREY
void CreatureCall( SOLDIERTYPE * pCaller )
{
UINT8 ubCallerType=0;
SoldierID ubReceiver;
INT8 bFullPriority;
INT8 bPriority;
SOLDIERTYPE *pReceiver;
UINT16 usDistToCaller;
// communicate call to all creatures on map through ultrasonics
gTacticalStatus.Team[pCaller->bTeam].bAwareOfOpposition = TRUE;
// bAction should be AI_ACTION_CREATURE_CALL (new)
// usActionData is call enum #
switch (pCaller->ubBodyType)
{
case ADULTFEMALEMONSTER:
case YAF_MONSTER:
ubCallerType = CALLER_FEMALE;
break;
case QUEENMONSTER:
ubCallerType = CALLER_QUEEN;
break;
// need to add male
case AM_MONSTER:
case YAM_MONSTER:
ubCallerType = CALLER_MALE;
break;
default:
ubCallerType = CALLER_FEMALE;
break;
}
if (pCaller->aiData.bHunting) // which should only be set for females outside of the hive
{
bFullPriority = gbHuntCallPriority[pCaller->aiData.usActionData];
}
else
{
bFullPriority = gbCallPriority[pCaller->aiData.usActionData][ubCallerType];
}
// OK, do animation based on body type...
switch (pCaller->ubBodyType)
{
case ADULTFEMALEMONSTER:
case YAF_MONSTER:
case AM_MONSTER:
case YAM_MONSTER:
pCaller->EVENT_InitNewSoldierAnim( MONSTER_UP, 0 , FALSE );
break;
case QUEENMONSTER:
pCaller->EVENT_InitNewSoldierAnim( QUEEN_CALL, 0 , FALSE );
break;
}
for (ubReceiver = gTacticalStatus.Team[ pCaller->bTeam ].bFirstID; ubReceiver <= gTacticalStatus.Team[ pCaller->bTeam ].bLastID; ++ubReceiver )
{
pReceiver = ubReceiver;
if (pReceiver->bActive && pReceiver->bInSector && (pReceiver->stats.bLife >= OKLIFE) && (pReceiver != pCaller) && (pReceiver->aiData.bAlertStatus < STATUS_BLACK))
{
if (pReceiver->ubBodyType != LARVAE_MONSTER && pReceiver->ubBodyType != INFANT_MONSTER && pReceiver->ubBodyType != QUEENMONSTER)
{
usDistToCaller = PythSpacesAway( pReceiver->sGridNo, pCaller->sGridNo );
bPriority = bFullPriority - (INT8) (usDistToCaller / PRIORITY_DECR_DISTANCE);
if (bPriority > pReceiver->aiData.bCallPriority)
{
pReceiver->aiData.bCallPriority = bPriority;
pReceiver->aiData.bAlertStatus = STATUS_RED; // our status can't be more than red to begin with
pReceiver->aiData.ubCaller = pCaller->ubID;
pReceiver->aiData.sCallerGridNo = pCaller->sGridNo;
pReceiver->aiData.bCallActedUpon = FALSE;
CancelAIAction(pReceiver, FORCE);
if ((bPriority > FRENZY_THRESHOLD) && (pReceiver->ubBodyType == ADULTFEMALEMONSTER || pReceiver->ubBodyType == YAF_MONSTER))
{
// go berzerk!
pReceiver->aiData.bFrenzied = TRUE;
}
}
}
}
}
}
INT8 CreatureDecideActionGreen( SOLDIERTYPE * pSoldier )
{
INT32 iChance, iSneaky = 10;
//INT8 bInWater;
INT8 bInGas;
//bInWater = pSoldier->MercInWater();
// NB creatures would ignore smoke completely :-)
if ( pSoldier->aiData.bMobility == CREATURE_CRAWLER && pSoldier->bActionPoints < pSoldier->bInitialActionPoints)
{
return( AI_ACTION_NONE );
}
bInGas = InGas( pSoldier, pSoldier->sGridNo );
if (pSoldier->aiData.bMobility == CREATURE_MOBILE)
{
if (TrackScent( pSoldier ))
{
return( AI_ACTION_TRACK );
}
////////////////////////////////////////////////////////////////////////////
// POINT PATROL: move towards next point unless getting a bit winded
////////////////////////////////////////////////////////////////////////////
// this takes priority over water/gas checks, so that point patrol WILL work
// from island to island, and through gas covered areas, too
if ((pSoldier->aiData.bOrders == POINTPATROL) && (pSoldier->bBreath >= 50))
{
if (PointPatrolAI(pSoldier))
{
if (!gfTurnBasedAI)
{
// pause at the end of the walk!
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = (UINT16) REALTIME_CREATURE_AI_DELAY;
}
return(AI_ACTION_POINT_PATROL);
}
}
if ((pSoldier->aiData.bOrders == RNDPTPATROL) && (pSoldier->bBreath >=50))
{
if (RandomPointPatrolAI(pSoldier))
{
if (!gfTurnBasedAI)
{
// pause at the end of the walk!
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = (UINT16) REALTIME_CREATURE_AI_DELAY;
}
return(AI_ACTION_POINT_PATROL);
}
}
////////////////////////////////////////////////////////////////////////////
// WHEN LEFT IN WATER OR GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND
////////////////////////////////////////////////////////////////////////////
if ( /*bInWater || */ bInGas)
{
pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - SEEKING NEAREST UNGASSED LAND at grid %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_LEAVE_WATER_GAS);
}
}
}
////////////////////////////////////////////////////////////////////////
// REST IF RUNNING OUT OF BREATH
////////////////////////////////////////////////////////////////////////
// if our breath is running a bit low, and we're not in the way or in water
if ((pSoldier->bBreath < 75) /*&& !bInWater*/)
{
// take a breather for gods sake!
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
////////////////////////////////////////////////////////////////////////////
// RANDOM PATROL: determine % chance to start a new patrol route
////////////////////////////////////////////////////////////////////////////
if (pSoldier->aiData.bMobility != CREATURE_IMMOBILE )
{
iChance = 25;
// set base chance according to orders
switch (pSoldier->aiData.bOrders)
{
case STATIONARY: iChance += -20; break;
case ONGUARD: iChance += -15; break;
case ONCALL: break;
case CLOSEPATROL: iChance += +15; break;
case RNDPTPATROL:
case POINTPATROL: iChance = 0; break;
case FARPATROL: iChance += +25; break;
case SEEKENEMY: iChance += -10; break;
}
// modify chance of patrol (and whether it's a sneaky one) by attitude
switch (pSoldier->aiData.bAttitude)
{
case DEFENSIVE: iChance += -10; break;
case BRAVESOLO: iChance += 5; break;
case BRAVEAID: break;
case CUNNINGSOLO: iChance += 5; iSneaky += 10; break;
case CUNNINGAID: iSneaky += 5; break;
case AGGRESSIVE: iChance += 10; iSneaky += -5; break;
}
// reduce chance for any injury, less likely to wander around when hurt
iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife);
// reduce chance if breath is down, less likely to wander around when tired
iChance -= (100 - pSoldier->bBreath);
// if we're in water with land miles (> 25 tiles) away,
// OR if we roll under the chance calculated
if ( /*bInWater ||*/ ((INT16) PreRandom(100) < iChance))
{
pSoldier->aiData.usActionData = RandDestWithinRange(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - RANDOM PATROL to grid %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
if (!gfTurnBasedAI)
{
// pause at the end of the walk!
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = (UINT16) REALTIME_CREATURE_AI_DELAY;
if (pSoldier->aiData.bMobility == CREATURE_CRAWLER)
{
pSoldier->aiData.usNextActionData *= 2;
}
}
return(AI_ACTION_RANDOM_PATROL);
}
}
/*
if (pSoldier->aiData.bMobility == CREATURE_MOBILE)
{
////////////////////////////////////////////////////////////////////////////
// SEEK FRIEND: determine %chance for man to pay a friendly visit
////////////////////////////////////////////////////////////////////////////
iChance = 25;
// set base chance and maximum seeking distance according to orders
switch (pSoldier->aiData.bOrders)
{
case STATIONARY: iChance += -20; break;
case ONGUARD: iChance += -15; break;
case ONCALL: break;
case CLOSEPATROL: iChance += +10; break;
case RNDPTPATROL:
case POINTPATROL: iChance = -10; break;
case FARPATROL: iChance += +20; break;
case SEEKENEMY: iChance += -10; break;
}
// modify for attitude
switch (pSoldier->aiData.bAttitude)
{
case DEFENSIVE: break;
case BRAVESOLO: iChance /= 2; break; // loners
case BRAVEAID: iChance += 10; break; // friendly
case CUNNINGSOLO: iChance /= 2; break; // loners
case CUNNINGAID: iChance += 10; break; // friendly
case AGGRESSIVE: break;
}
// reduce chance for any injury, less likely to wander around when hurt
iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife);
// reduce chance if breath is down
iChance -= (100 - pSoldier->bBreath); // very likely to wait when exhausted
if ((INT16) PreRandom(100) < iChance)
{
if (RandomFriendWithin(pSoldier))
{
#ifdef DEBUGDECISIONS
sprintf(tempstr,"%s - SEEK FRIEND at grid %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
if (!gfTurnBasedAI)
{
// pause at the end of the walk!
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = (UINT16) REALTIME_CREATURE_AI_DELAY;
}
return(AI_ACTION_SEEK_FRIEND);
}
}
}
*/
////////////////////////////////////////////////////////////////////////////
// LOOK AROUND: determine %chance for man to turn in place
////////////////////////////////////////////////////////////////////////////
// avoid 2 consecutive random turns in a row
if (pSoldier->aiData.bLastAction != AI_ACTION_CHANGE_FACING && (GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints))
{
iChance = 25;
// set base chance according to orders
if (pSoldier->aiData.bOrders == STATIONARY)
iChance += 25;
if (pSoldier->aiData.bOrders == ONGUARD)
iChance += 20;
if (pSoldier->aiData.bAttitude == DEFENSIVE)
iChance += 25;
if ((INT16)PreRandom(100) < iChance)
{
// roll random directions (stored in actionData) until different from current
do
{
// if man has a LEGAL dominant facing, and isn't facing it, he will turn
// back towards that facing 50% of the time here (normally just enemies)
if ((pSoldier->aiData.bDominantDir >= 0) && (pSoldier->aiData.bDominantDir <= 8) &&
(pSoldier->ubDirection != pSoldier->aiData.bDominantDir) && PreRandom(2))
{
pSoldier->aiData.usActionData = pSoldier->aiData.bDominantDir;
}
else
{
pSoldier->aiData.usActionData = (UINT16)PreRandom(8);
}
} while (pSoldier->aiData.usActionData == pSoldier->ubDirection);
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - TURNS to face direction %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
if ( ValidCreatureTurn( pSoldier, (INT8) pSoldier->aiData.usActionData ) )
//pSoldier->InternalIsValidStance( (INT8) pSoldier->usActionData, ANIM_STAND ) )
{
if (!gfTurnBasedAI)
{
// pause at the end of the turn!
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = (UINT16) REALTIME_CREATURE_AI_DELAY;
}
return(AI_ACTION_CHANGE_FACING);
}
}
}
}
////////////////////////////////////////////////////////////////////////////
// NONE:
////////////////////////////////////////////////////////////////////////////
// by default, if everything else fails, just stands in place without turning
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
INT8 CreatureDecideActionYellow( SOLDIERTYPE * pSoldier )
{
// monster AI - heard something
UINT8 ubNoiseDir;
INT32 sNoiseGridNo;
INT32 iNoiseValue;
INT32 iChance, iSneaky;
BOOLEAN fClimb;
BOOLEAN fReachable;
// INT16 sClosestFriend;
if ( pSoldier->aiData.bMobility == CREATURE_CRAWLER && pSoldier->bActionPoints < pSoldier->bInitialActionPoints)
{
return( AI_ACTION_NONE );
}
// determine the most important noise heard, and its relative value
sNoiseGridNo = MostImportantNoiseHeard(pSoldier,&iNoiseValue,&fClimb, &fReachable);
//NumMessage("iNoiseValue = ",iNoiseValue);
if (TileIsOutOfBounds(sNoiseGridNo))
{
// then we have no business being under YELLOW status any more!
#ifdef RECORDNET
fprintf(NetDebugFile,"\nDecideActionYellow: ERROR - No important noise known by guynum %d\n\n",pSoldier->ubID);
#endif
#ifdef BETAVERSION
NumMessage("DecideActionYellow: ERROR - No important noise known by guynum ",pSoldier->ubID);
#endif
return(AI_ACTION_NONE);
}
////////////////////////////////////////////////////////////////////////////
// LOOK AROUND TOWARD NOISE: determine %chance for man to turn towards noise
////////////////////////////////////////////////////////////////////////////
if (pSoldier->aiData.bMobility != CREATURE_IMMOBILE)
{
// determine direction from this soldier in which the noise lies
ubNoiseDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sNoiseGridNo);
// if soldier is not already facing in that direction,
// and the noise source is close enough that it could possibly be seen
if ((GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints) && (pSoldier->ubDirection != ubNoiseDir) && PythSpacesAway(pSoldier->sGridNo,sNoiseGridNo) <= STRAIGHT)
{
// set base chance according to orders
if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD))
iChance = 60;
else // all other orders
iChance = 35;
if (pSoldier->aiData.bAttitude == DEFENSIVE)
iChance += 15;
if ((INT16)PreRandom(100) < iChance)
{
pSoldier->aiData.usActionData = ubNoiseDir;
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - TURNS TOWARDS NOISE to face direction %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
//if ( pSoldier->InternalIsValidStance( (INT8) pSoldier->usActionData, ANIM_STAND ) )
if ( ValidCreatureTurn( pSoldier, (INT8) pSoldier->aiData.usActionData ) )
{
return(AI_ACTION_CHANGE_FACING);
}
}
}
}
////////////////////////////////////////////////////////////////////////
// REST IF RUNNING OUT OF BREATH
////////////////////////////////////////////////////////////////////////
// if our breath is running a bit low, and we're not in water
if ((pSoldier->bBreath < 25) /*&& !pSoldier->MercInWater() */ )
{
// take a breather for gods sake!
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
if (pSoldier->aiData.bMobility != CREATURE_IMMOBILE && fReachable)
{
////////////////////////////////////////////////////////////////////////////
// SEEK NOISE
////////////////////////////////////////////////////////////////////////////
// remember that noise value is negative, and closer to 0 => more important!
iChance = 75 + iNoiseValue;
iSneaky = 30;
// set base chance according to orders
switch (pSoldier->aiData.bOrders)
{
case STATIONARY: iChance += -20; break;
case ONGUARD: iChance += -15; break;
case ONCALL: break;
case CLOSEPATROL: iChance += -10; break;
case RNDPTPATROL:
case POINTPATROL: break;
case FARPATROL: iChance += 10; break;
case SEEKENEMY: iChance += 25; break;
}
// modify chance of patrol (and whether it's a sneaky one) by attitude
switch (pSoldier->aiData.bAttitude)
{
case DEFENSIVE: iChance += -10; iSneaky += 15; break;
case BRAVESOLO: iChance += 10; break;
case BRAVEAID: iChance += 5; break;
case CUNNINGSOLO: iChance += 5; iSneaky += 30; break;
case CUNNINGAID: iSneaky += 30; break;
case AGGRESSIVE: iChance += 20; iSneaky += -10; break;
}
// reduce chance if breath is down, less likely to wander around when tired
iChance -= (100 - pSoldier->bBreath);
if ((INT16) PreRandom(100) < iChance)
{
pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier,sNoiseGridNo, AI_ACTION_SEEK_NOISE);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - INVESTIGATING NOISE at grid %d, moving to %d",
pSoldier->name,sNoiseGridNo,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_SEEK_NOISE);
}
}
// Okay, we're not following up on the noise... but let's follow any
// scent trails available
if (TrackScent( pSoldier ))
{
return( AI_ACTION_TRACK );
}
}
////////////////////////////////////////////////////////////////////////////
// DO NOTHING: Not enough points left to move, so save them for next turn
////////////////////////////////////////////////////////////////////////////
#ifdef DEBUGDECISIONS
AINameMessage(pSoldier,"- DOES NOTHING (YELLOW)",1000);
#endif
// by default, if everything else fails, just stands in place without turning
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
INT8 CreatureDecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK)
{
// monster AI - hostile mammals somewhere around!
INT32 iChance, sClosestOpponent /*,sClosestOpponent,sClosestFriend*/;
INT32 sClosestDisturbance;
INT32 sDistVisible;
UINT8 ubCanMove,ubOpponentDir;
//INT8 bInWater;
INT8 bInGas;
INT8 bSeekPts = 0, bHelpPts = 0, bHidePts = 0;
INT32 sAdjustedGridNo;
BOOLEAN fChangeLevel;
// if we have absolutely no action points, we can't do a thing under RED!
if (!pSoldier->bActionPoints)
{
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
if ( pSoldier->aiData.bMobility == CREATURE_CRAWLER && pSoldier->bActionPoints < pSoldier->bInitialActionPoints)
{
return( AI_ACTION_NONE );
}
// can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE)
ubCanMove = ((pSoldier->aiData.bMobility != CREATURE_IMMOBILE) && (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)));
// determine if we happen to be in water (in which case we're in BIG trouble!)
//bInWater = pSoldier->MercInWater();
// check if standing in tear gas without a gas mask on
bInGas = InGas( pSoldier, pSoldier->sGridNo );
////////////////////////////////////////////////////////////////////////////
// WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND
////////////////////////////////////////////////////////////////////////////
if (bInGas && ubCanMove)
{
pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - SEEKING NEAREST UNGASSED LAND at grid %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_LEAVE_WATER_GAS);
}
}
////////////////////////////////////////////////////////////////////////////
// CALL FOR AID IF HURT
////////////////////////////////////////////////////////////////////////////
if ( CAN_CALL( pSoldier ) )
{
if ((pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1))
{
if (pSoldier->stats.bLife < pSoldier->bOldLife)
{
// got injured, maybe call
if ((pSoldier->bOldLife == pSoldier->stats.bLifeMax) && (pSoldier->bOldLife - pSoldier->stats.bLife > 10))
{
// hurt for first time!
pSoldier->aiData.usActionData = CALL_CRIPPLED;
pSoldier->bOldLife = pSoldier->stats.bLife; // don't want to call more than once
return(AI_ACTION_CREATURE_CALL);
}
else if (pSoldier->stats.bLifeMax / pSoldier->stats.bLife > 2)
{
// crippled, 1/3 or less health!
pSoldier->aiData.usActionData = CALL_ATTACKED;
pSoldier->bOldLife = pSoldier->stats.bLife; // don't want to call more than once
return(AI_ACTION_CREATURE_CALL);
}
}
}
}
////////////////////////////////////////////////////////////////////////
// CROUCH & REST IF RUNNING OUT OF BREATH
////////////////////////////////////////////////////////////////////////
// if our breath is running a bit low, and we're not in water or under fire
if ((pSoldier->bBreath < 25) /*&& !bInWater*/ && !pSoldier->aiData.bUnderFire)
{
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s RESTS (STATUS RED), breath = %d",pSoldier->name,pSoldier->bBreath);
AIPopMessage(tempstr);
#endif
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
////////////////////////////////////////////////////////////////////////////
// CALL IN SIGHTING: determine %chance to call others and report contact
////////////////////////////////////////////////////////////////////////////
// if we're a computer merc, and we have the action points remaining to RADIO
// (we never want NPCs to choose to radio if they would have to wait a turn)
if ( CAN_CALL( pSoldier ) && (!gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) )
{
if ((pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1))
{
// if there hasn't been a general sighting call sent yet
// might want to check the specifics of who we see
iChance = 20;
if (iChance)
{
#ifdef DEBUGDECISIONS
AINumMessage("Chance to call sighting = ",iChance);
#endif
if ((INT16) PreRandom(100) < iChance)
{
#ifdef DEBUGDECISIONS
AINameMessage(pSoldier,"decides to call an alert!",1000);
#endif
pSoldier->aiData.usActionData = CALL_1_PREY;
return(AI_ACTION_CREATURE_CALL);
}
}
}
}
if ( pSoldier->aiData.bMobility != CREATURE_IMMOBILE )
{
if ( FindAIUsableObjClass( pSoldier, IC_WEAPON ) == ITEM_NOT_FOUND )
{
// probably a baby bug... run away! run away!
// look for best place to RUN AWAY to (farthest from the closest threat)
pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents( pSoldier );
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
return(AI_ACTION_RUN_AWAY);
}
else
{
return( AI_ACTION_NONE );
}
}
// Respond to call if any
if ( CAN_LISTEN_TO_CALL( pSoldier ) && pSoldier->aiData.ubCaller != NOBODY )
{
if ( PythSpacesAway( pSoldier->sGridNo, pSoldier->aiData.sCallerGridNo ) <= STOPSHORTDIST )
{
// call completed... hmm, nothing found
pSoldier->aiData.ubCaller = NOBODY;
}
else
{
pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.sCallerGridNo, -1, AI_ACTION_SEEK_FRIEND, FLAG_STOPSHORT);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - SEEKING FRIEND at %d, MOVING to %d",
pSoldier->name,sClosestFriend,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_SEEK_FRIEND);
}
}
}
// get the location of the closest reachable opponent
sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fChangeLevel);
// if there is an opponent reachable
if (!TileIsOutOfBounds(sClosestDisturbance))
{
//////////////////////////////////////////////////////////////////////
// SEEK CLOSEST DISTURBANCE: GO DIRECTLY TOWARDS CLOSEST KNOWN OPPONENT
//////////////////////////////////////////////////////////////////////
// try to move towards him
pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier,sClosestDisturbance,AI_ACTION_SEEK_OPPONENT);
// if it's possible
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
#ifdef DEBUGDECISIONS
// do it!
STR16 tempstr;
sprintf(tempstr,"%s - SEEKING OPPONENT at grid %d, MOVING to %d",
pSoldier->name,sClosestDisturbance,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_SEEK_OPPONENT);
}
}
////////////////////////////////////////////////////////////////////////////
// TAKE A BITE, PERHAPS
////////////////////////////////////////////////////////////////////////////
if (pSoldier->aiData.bHunting)
{
pSoldier->aiData.usActionData = FindNearestRottingCorpse( pSoldier );
// need smell/visibility check?
if (PythSpacesAway( pSoldier->sGridNo, pSoldier->aiData.usActionData) < MAX_EAT_DIST )
{
INT32 sGridNo;
sGridNo = FindAdjacentGridEx( pSoldier, pSoldier->aiData.usActionData, &ubOpponentDir, &sAdjustedGridNo, FALSE, FALSE );
if ( sGridNo != -1 )
{
pSoldier->aiData.usActionData = sGridNo;
return( AI_ACTION_APPROACH_MERC );
}
}
}
////////////////////////////////////////////////////////////////////////////
// TRACK A SCENT, IF ONE IS PRESENT
////////////////////////////////////////////////////////////////////////////
if (TrackScent( pSoldier ))
{
return( AI_ACTION_TRACK );
}
////////////////////////////////////////////////////////////////////////////
// LOOK AROUND TOWARD CLOSEST KNOWN OPPONENT, IF KNOWN
////////////////////////////////////////////////////////////////////////////
if (GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints)
{
// determine the location of the known closest opponent
// (don't care if he's conscious, don't care if he's reachable at all)
INT32 distanceToThreat;
sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL, NULL, &distanceToThreat);
if (!TileIsOutOfBounds(sClosestOpponent))
{
// determine direction from this soldier to the closest opponent
ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent);
// if soldier is not already facing in that direction,
// and the opponent is close enough that he could possibly be seen
// note, have to change this to use the level returned from ClosestKnownOpponent
sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestOpponent, 0 ) * CELL_X_SIZE;
if ((pSoldier->ubDirection != ubOpponentDir) && (distanceToThreat <= sDistVisible))
{
// set base chance according to orders
if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD))
iChance = 50;
else // all other orders
iChance = 25;
if (pSoldier->aiData.bAttitude == DEFENSIVE)
iChance += 25;
//if ( (INT16)PreRandom(100) < iChance && pSoldier->InternalIsValidStance( ubOpponentDir, ANIM_STAND ) )
if ( (INT16)PreRandom(100) < iChance && ValidCreatureTurn( pSoldier, ubOpponentDir ) )
{
pSoldier->aiData.usActionData = ubOpponentDir;
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - TURNS TOWARDS CLOSEST ENEMY to face direction %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_CHANGE_FACING);
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////
// LEAVE THE SECTOR
////////////////////////////////////////////////////////////////////////////
// NOT IMPLEMENTED
////////////////////////////////////////////////////////////////////////////
// DO NOTHING: Not enough points left to move, so save them for next turn
////////////////////////////////////////////////////////////////////////////
#ifdef DEBUGDECISIONS
AINameMessage(pSoldier,"- DOES NOTHING (RED)",1000);
#endif
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
INT8 CreatureDecideActionBlack( SOLDIERTYPE * pSoldier )
{
// monster AI - hostile mammals in sense range
INT32 sClosestOpponent,sBestCover = NOWHERE;
INT32 sClosestDisturbance;
INT16 ubMinAPCost;
UINT8 ubCanMove/*,bInWater*/,bInGas;
INT8 bDirection;
UINT8 ubBestAttackAction;
INT8 bCanAttack;
INT8 bSpitIn, bWeaponIn;
UINT32 uiChance;
ATTACKTYPE BestShot, BestStab, BestAttack, CurrStab;//dnl ch69 150913
BOOLEAN fRunAway = FALSE;
BOOLEAN fChangeLevel;
// if we have absolutely no action points, we can't do a thing under BLACK!
if (!pSoldier->bActionPoints)
{
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
if ( pSoldier->aiData.bMobility == CREATURE_CRAWLER && pSoldier->bActionPoints < pSoldier->bInitialActionPoints)
{
return( AI_ACTION_NONE );
}
////////////////////////////////////////////////////////////////////////////
// CALL FOR AID IF HURT OR IF OTHERS ARE UNAWARE
////////////////////////////////////////////////////////////////////////////
if ( CAN_CALL( pSoldier ) )
{
if ((pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1))
{
if (pSoldier->stats.bLife < pSoldier->bOldLife)
{
// got injured, maybe call
/*
// don't call when crippled and have target... save breath for attacking!
if ((pSoldier->bOldLife == pSoldier->stats.bLifeMax) && (pSoldier->bOldLife - pSoldier->stats.bLife > 10))
{
// hurt for first time!
pSoldier->aiData.usActionData = CALL_CRIPPLED;
pSoldier->bOldLife = pSoldier->stats.bLife; // don't want to call more than once
return(AI_ACTION_CREATURE_CALL);
}
else
*/
if (pSoldier->stats.bLifeMax / pSoldier->stats.bLife > 2)
{
// crippled, 1/3 or less health!
pSoldier->aiData.usActionData = CALL_ATTACKED;
pSoldier->bOldLife = pSoldier->stats.bLife; // don't want to call more than once
return(AI_ACTION_CREATURE_CALL);
}
}
else
{
if (!(gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition))
{
if (pSoldier->ubBodyType == QUEENMONSTER)
{
uiChance = 100;
}
else
{
uiChance = 20 * pSoldier->aiData.bOppCnt;
}
if ( Random( 100 ) < uiChance )
{
// alert! alert!
if (pSoldier->aiData.bOppCnt > 1)
{
pSoldier->aiData.usActionData = CALL_MULTIPLE_PREY;
}
else
{
pSoldier->aiData.usActionData = CALL_1_PREY;
}
return(AI_ACTION_CREATURE_CALL);
}
}
}
}
}
// can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE)
ubCanMove = ((pSoldier->aiData.bMobility != CREATURE_IMMOBILE) && (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)));
// determine if we happen to be in water (in which case we're in BIG trouble!)
//bInWater = pSoldier->MercInWater();
// check if standing in tear gas without a gas mask on
bInGas = InGas( pSoldier, pSoldier->sGridNo );
////////////////////////////////////////////////////////////////////////////
// IF GASSED, OR REALLY TIRED (ON THE VERGE OF COLLAPSING), TRY TO RUN AWAY
////////////////////////////////////////////////////////////////////////////
// if we're desperately short on breath (it's OK if we're in water, though!)
if (bInGas || (pSoldier->bBreath < 5))
{
// if soldier has enough APs left to move at least 1 square's worth
if (ubCanMove)
{
// look for best place to RUN AWAY to (farthest from the closest threat)
pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - GASSED or LOW ON BREATH (%d), RUNNING AWAY to grid %d",pSoldier->name,pSoldier->bBreath,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_RUN_AWAY);
}
}
}
////////////////////////////////////////////////////////////////////////////
// STUCK IN WATER OR GAS, NO COVER, GO TO NEAREST SPOT OF UNGASSED LAND
////////////////////////////////////////////////////////////////////////////
// if soldier in water/gas has enough APs left to move at least 1 square
if ((/*bInWater ||*/ bInGas) && ubCanMove)
{
pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - SEEKING NEAREST UNGASSED LAND at grid %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_LEAVE_WATER_GAS);
}
}
////////////////////////////////////////////////////////////////////////////
// SOLDIER CAN ATTACK IF NOT IN WATER/GAS AND NOT DOING SOMETHING TOO FUNKY
////////////////////////////////////////////////////////////////////////////
// NPCs in water/tear gas without masks are not permitted to shoot/stab/throw
if ((pSoldier->bActionPoints < 2) /*|| bInWater*/ || bInGas)
{
bCanAttack = FALSE;
}
else
{
bCanAttack = CanNPCAttack(pSoldier);
if (bCanAttack != TRUE)
{
if ( bCanAttack == NOSHOOT_NOAMMO )
{
pSoldier->inv[HANDPOS].fFlags |= OBJECT_AI_UNUSABLE;
// try to find a bladed weapon
if (pSoldier->ubBodyType == QUEENMONSTER)
{
bWeaponIn = FindObjClass( pSoldier, IC_TENTACLES );
}
else
{
bWeaponIn = FindObjClass( pSoldier, IC_BLADE );
}
if ( bWeaponIn != NO_SLOT )
{
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"creaturedecideactionblack: swapping item into hand");
RearrangePocket(pSoldier,HANDPOS,bWeaponIn,FOREVER);
bCanAttack = TRUE;
}
else
{
// infants who exhaust their spit should flee!
fRunAway = TRUE;
bCanAttack = FALSE;
}
}
else
{
bCanAttack = FALSE;
}
}
}
BestShot.ubPossible = FALSE; // by default, assume Shooting isn't possible
BestStab.ubPossible = FALSE; // by default, assume Stabbing isn't possible
BestAttack.ubChanceToReallyHit = 0;
bSpitIn = NO_SLOT;
// if we are able attack
if (bCanAttack)
{
//////////////////////////////////////////////////////////////////////////
// FIRE A GUN AT AN OPPONENT
//////////////////////////////////////////////////////////////////////////
pSoldier->bAimShotLocation = AIM_SHOT_RANDOM;
bWeaponIn = FindObjClass( pSoldier, IC_GUN );
if (bWeaponIn != NO_SLOT)
{
if (Item[pSoldier->inv[bWeaponIn].usItem].usItemClass == IC_GUN && pSoldier->inv[bWeaponIn][0]->data.gun.bGunStatus >= USABLE)
{
if (pSoldier->inv[bWeaponIn][0]->data.gun.ubGunShotsLeft > 0)
{
bSpitIn = bWeaponIn;
// if it's in another pocket, swap it into his hand temporarily
if (bWeaponIn != HANDPOS)
{
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"creaturedecideactionblack: swapping gun into hand");
RearrangePocket(pSoldier,HANDPOS,bWeaponIn,TEMPORARILY);
}
// now it better be a gun, or the guy can't shoot (but has other attack(s))
// get the minimum cost to attack the same target with this gun
ubMinAPCost = MinAPsToAttack(pSoldier,pSoldier->sLastTarget,DONTADDTURNCOST,0);
// if we have enough action points to shoot with this gun
if (pSoldier->bActionPoints >= ubMinAPCost)
{
// look around for a worthy target (which sets BestShot.ubPossible)
CalcBestShot(pSoldier,&BestShot);
if (BestShot.ubPossible)
{
BestShot.bWeaponIn = bWeaponIn;
// if the selected opponent is not a threat (unconscious & !serviced)
// (usually, this means all the guys we see our unconscious, but, on
// rare occasions, we may not be able to shoot a healthy guy, too)
if ((BestShot.ubOpponent->stats.bLife < OKLIFE) &&
!BestShot.ubOpponent->bService)
{
// if our attitude is NOT aggressive
if (pSoldier->aiData.bAttitude != AGGRESSIVE)
{
// get the location of the closest CONSCIOUS reachable opponent
sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fChangeLevel);
// if we found one
if (!TileIsOutOfBounds(sClosestDisturbance))
{
// don't bother checking GRENADES/KNIVES, he can't have conscious targets
#ifdef RECORDNET
fprintf(NetDebugFile,"\tDecideActionBlack: all visible opponents unconscious, switching to RED AI...\n");
#endif
// then make decision as if at alert status RED, but make sure
// we don't try to SEEK OPPONENT the unconscious guy!
return(CreatureDecideActionRed(pSoldier,FALSE));
}
// else kill the guy, he could be the last opponent alive in this sector
}
// else aggressive guys will ALWAYS finish off unconscious opponents
}
// now we KNOW FOR SURE that we will do something (shoot, at least)
NPCDoesAct(pSoldier);
}
}
// if it was in his holster, swap it back into his holster for now
if (bWeaponIn != HANDPOS)
{
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"creaturedecideactionblack: swapping gun into holster");
RearrangePocket(pSoldier,HANDPOS,bWeaponIn,TEMPORARILY);
}
}
else
{
// out of ammo! reload if possible!
}
}
}
//////////////////////////////////////////////////////////////////////////
// GO STAB AN OPPONENT WITH A KNIFE
//////////////////////////////////////////////////////////////////////////
// if soldier has a knife in his hand
if (pSoldier->ubBodyType == QUEENMONSTER)
{
bWeaponIn = FindObjClass( pSoldier, IC_TENTACLES );
}
else if ( pSoldier->ubBodyType == BLOODCAT )
{
// 1 in 3 attack with teeth, otherwise with claws
if ( PreRandom( 3 ) )
{
bWeaponIn = FindObj( pSoldier, BLOODCAT_CLAW_ATTACK );
}
else
{
bWeaponIn = FindObj( pSoldier, BLOODCAT_BITE );
}
}
else
{
if (bSpitIn != NO_SLOT && Random( 4 ) )
{
// spitters only consider a blade attack 1 time in 4
bWeaponIn = NO_SLOT;
}
else
{
bWeaponIn = FindObjClass( pSoldier, IC_BLADE );
}
}
BestStab.iAttackValue = 0;
// if the soldier does have a usable knife somewhere
// spitters don't always consider using their claws
if ( bWeaponIn != NO_SLOT )
{
// if it's in his holster, swap it into his hand temporarily
if (bWeaponIn != HANDPOS)
{
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"creaturedecideactionblack: swapping knife into hand");
RearrangePocket(pSoldier,HANDPOS,bWeaponIn,TEMPORARILY);
}
// get the minimum cost to attack with this knife
ubMinAPCost = MinAPsToAttack(pSoldier,pSoldier->sLastTarget,DONTADDTURNCOST,0);
//sprintf(tempstr,"%s - ubMinAPCost = %d",pSoldier->name,ubMinAPCost);
//PopMessage(tempstr);
// if we can afford the minimum AP cost to stab with this knife weapon
if (pSoldier->bActionPoints >= ubMinAPCost)
{
// then look around for a worthy target (which sets BestStab.ubPossible)
if (pSoldier->ubBodyType == QUEENMONSTER)
{
CalcTentacleAttack( pSoldier, &CurrStab );
}
else
{
CalcBestStab(pSoldier, &CurrStab, TRUE);
}
if (CurrStab.ubPossible)
{
// now we KNOW FOR SURE that we will do something (stab, at least)
NPCDoesAct(pSoldier);
}
// if it was in his holster, swap it back into his holster for now
if (bWeaponIn != HANDPOS)
{
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"creaturedecideactionblack: swapping knife into holster");
RearrangePocket(pSoldier,HANDPOS,bWeaponIn,TEMPORARILY);
}
if (CurrStab.iAttackValue > BestStab.iAttackValue)
{
CurrStab.bWeaponIn = bWeaponIn;
memcpy(&BestStab,&CurrStab,sizeof(BestStab));
}
}
}
//////////////////////////////////////////////////////////////////////////
// CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE
//////////////////////////////////////////////////////////////////////////
if (BestShot.ubPossible)
{
BestAttack.iAttackValue = BestShot.iAttackValue;
ubBestAttackAction = AI_ACTION_FIRE_GUN;
}
else
{
BestAttack.iAttackValue = 0;
ubBestAttackAction = AI_ACTION_NONE;
}
if (BestStab.ubPossible && BestStab.iAttackValue > (BestAttack.iAttackValue * 12) / 10 )
{
BestAttack.iAttackValue = BestStab.iAttackValue;
ubBestAttackAction = AI_ACTION_KNIFE_MOVE;
}
// if attack is still desirable (meaning it's also preferred to taking cover)
if (ubBestAttackAction != AI_ACTION_NONE)
{
// copy the information on the best action selected into BestAttack struct
switch (ubBestAttackAction)
{
case AI_ACTION_FIRE_GUN:
memcpy(&BestAttack,&BestShot,sizeof(BestAttack));
break;
case AI_ACTION_KNIFE_MOVE:
memcpy(&BestAttack,&BestStab,sizeof(BestAttack));
break;
}
// if necessary, swap the weapon into the hand position
if (BestAttack.bWeaponIn != HANDPOS)
{
// IS THIS NOT BEING SET RIGHT?????
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"creaturedecideactionblack: swapping best weapon into hand");
RearrangePocket(pSoldier,HANDPOS,BestAttack.bWeaponIn,FOREVER);
}
//////////////////////////////////////////////////////////////////////////
// GO AHEAD & ATTACK!
//////////////////////////////////////////////////////////////////////////
pSoldier->aiData.usActionData = BestAttack.sTarget;
//POSSIBLE STRUCTURE PROBLEM WITH INT8/INT16. GOTTHARD 7/14/08
pSoldier->aiData.bAimTime = BestAttack.ubAimTime;
if ( ubBestAttackAction == AI_ACTION_FIRE_GUN && BestAttack.ubChanceToReallyHit > 50 )
{
pSoldier->bAimShotLocation = AIM_SHOT_HEAD;
}
else
{
pSoldier->bAimShotLocation = AIM_SHOT_RANDOM;
}
#ifdef DEBUGDECISIONS
STR tempstr;
sprintf( tempstr, "%d(%s) %s %d(%s) at gridno %d (%d APs aim)\n",
pSoldier->ubID,pSoldier->name,
(ubBestAttackAction == AI_ACTION_FIRE_GUN)?"SHOOTS":((ubBestAttackAction == AI_ACTION_TOSS_PROJECTILE)?"TOSSES AT":"STABS"),
BestAttack.ubOpponent,ExtMen[BestAttack.ubOpponent].name,
BestAttack.sTarget,BestAttack.ubAimTime) ;
DebugAI( tempstr );
#endif
return(ubBestAttackAction);
}
}
////////////////////////////////////////////////////////////////////////////
// CLOSE ON THE CLOSEST KNOWN OPPONENT or TURN TO FACE HIM
////////////////////////////////////////////////////////////////////////////
if ( !fRunAway )
{
if ( (GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints) )
{
// determine the location of the known closest opponent
// (don't care if he's conscious, don't care if he's reachable at all)
INT32 distanceToThreat;
sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL, NULL, &distanceToThreat);
// if we have a closest reachable opponent
if (!TileIsOutOfBounds(sClosestOpponent))
{
if ( ubCanMove && distanceToThreat > 20 ) // 2 tiles -> 20 in fractional Cell Coordinates
{
if ( bSpitIn != NO_SLOT )
{
pSoldier->aiData.usActionData = AdvanceToFiringRange( pSoldier, sClosestOpponent );
if (TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier,sClosestOpponent,AI_ACTION_SEEK_OPPONENT);
}
}
else
{
pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier,sClosestOpponent,AI_ACTION_SEEK_OPPONENT);
}
}
else
{
pSoldier->aiData.usActionData = NOWHERE;
}
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) // charge!
{
return( AI_ACTION_SEEK_OPPONENT );
}
else if (GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints) // turn to face enemy
{
bDirection = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent);
// if we're not facing towards him
if (pSoldier->ubDirection != bDirection && ValidCreatureTurn( pSoldier, bDirection ) )
{
pSoldier->aiData.usActionData = bDirection;
#ifdef DEBUGDECISIONS
STR16 tempstr;
sprintf(tempstr,"%s - TURNS to face CLOSEST OPPONENT in direction %d",pSoldier->name,pSoldier->aiData.usActionData);
AIPopMessage(tempstr);
#endif
return(AI_ACTION_CHANGE_FACING);
}
}
}
}
}
else
{
// run away!
if ( ubCanMove )
{
// look for best place to RUN AWAY to (farthest from the closest threat)
//pSoldier->aiData.usActionData = RunAway( pSoldier );
pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents( pSoldier );
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
return(AI_ACTION_RUN_AWAY);
}
}
}
////////////////////////////////////////////////////////////////////////////
// DO NOTHING: Not enough points left to move, so save them for next turn
////////////////////////////////////////////////////////////////////////////
#ifdef DEBUGDECISIONS
AINameMessage(pSoldier,"- DOES NOTHING (BLACK)",1000);
#endif
// by default, if everything else fails, just stand in place and wait
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
INT8 CreatureDecideAction( SOLDIERTYPE *pSoldier )
{
INT8 bAction = AI_ACTION_NONE;
switch (pSoldier->aiData.bAlertStatus)
{
case STATUS_GREEN:
#ifdef DEBUGDECISIONS
AIPopMessage("AlertStatus = GREEN");
#endif
bAction = CreatureDecideActionGreen(pSoldier);
break;
case STATUS_YELLOW:
#ifdef DEBUGDECISIONS
AIPopMessage("AlertStatus = YELLOW");
#endif
bAction = CreatureDecideActionYellow(pSoldier);
break;
case STATUS_RED:
#ifdef DEBUGDECISIONS
AIPopMessage("AlertStatus = RED");
#endif
bAction = CreatureDecideActionRed(pSoldier, TRUE);
break;
case STATUS_BLACK:
#ifdef DEBUGDECISIONS
AIPopMessage("AlertStatus = BLACK");
#endif
bAction = CreatureDecideActionBlack(pSoldier);
break;
}
#ifdef DEBUGDECISIONS
STR tempstr;
sprintf( tempstr, "DecideAction: selected action %d, actionData %d\n\n",bAction,pSoldier->aiData.usActionData );
DebugAI(tempstr);
#endif
return(bAction);
}
void CreatureDecideAlertStatus( SOLDIERTYPE *pSoldier )
{
INT8 bOldStatus;
INT32 iDummy;
BOOLEAN fClimbDummy, fReachableDummy;
// THE FOUR (4) POSSIBLE ALERT STATUSES ARE:
// GREEN - No one sensed, no suspicious noise heard, go about doing regular stuff
// YELLOW - Suspicious noise was heard personally
// RED - Either saw OPPONENTS in person, or definite contact had been called
// BLACK - Currently has one or more OPPONENTS in sight
// set mobility
switch (pSoldier->ubBodyType)
{
case ADULTFEMALEMONSTER:
case YAF_MONSTER:
case AM_MONSTER:
case YAM_MONSTER:
case INFANT_MONSTER:
pSoldier->aiData.bMobility = CREATURE_MOBILE;
break;
case QUEENMONSTER:
pSoldier->aiData.bMobility = CREATURE_IMMOBILE;
break;
case LARVAE_MONSTER:
pSoldier->aiData.bMobility = CREATURE_CRAWLER;
break;
}
if (pSoldier->ubBodyType == LARVAE_MONSTER)
{
// larvae never do anything much!
pSoldier->aiData.bAlertStatus = STATUS_GREEN;
return;
}
// save the man's previous status
bOldStatus = pSoldier->aiData.bAlertStatus;
// determine the current alert status for this category of man
if (pSoldier->aiData.bOppCnt > 0) // opponent(s) in sight
{
// must search through list of people to see if any of them have
// attacked us, or do some check to see if we have been attacked
switch (bOldStatus)
{
case STATUS_GREEN:
case STATUS_YELLOW:
pSoldier->aiData.bAlertStatus = STATUS_BLACK;
break;
case STATUS_RED:
case STATUS_BLACK:
pSoldier->aiData.bAlertStatus = STATUS_BLACK;
}
}
else // no opponents are in sight
{
switch (bOldStatus)
{
case STATUS_BLACK:
// then drop back to RED status
pSoldier->aiData.bAlertStatus = STATUS_RED;
break;
case STATUS_RED:
// RED can never go back down below RED, only up to BLACK
break;
case STATUS_YELLOW:
// if all enemies have been RED alerted, or we're under fire
if (gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition || pSoldier->aiData.bUnderFire)
{
pSoldier->aiData.bAlertStatus = STATUS_RED;
}
else
{
// if we are NOT aware of any uninvestigated noises right now
// and we are not currently in the middle of an action
// (could still be on his way heading to investigate a noise!)
if (( TileIsOutOfBounds(MostImportantNoiseHeard(pSoldier,&iDummy,&fClimbDummy,&fReachableDummy))) && !pSoldier->aiData.bActionInProgress)
{
// then drop back to GREEN status
pSoldier->aiData.bAlertStatus = STATUS_GREEN;
}
}
break;
case STATUS_GREEN:
// if all enemies have been RED alerted, or we're under fire
if (gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition || pSoldier->aiData.bUnderFire)
{
pSoldier->aiData.bAlertStatus = STATUS_RED;
}
else
{
// if we ARE aware of any uninvestigated noises right now
if ( !TileIsOutOfBounds(MostImportantNoiseHeard(pSoldier,&iDummy,&fClimbDummy,&fReachableDummy)))
{
// then move up to YELLOW status
pSoldier->aiData.bAlertStatus = STATUS_YELLOW;
}
}
break;
}
// otherwise, RED stays RED, YELLOW stays YELLOW, GREEN stays GREEN
}
// if the creatures alert status has changed in any way
if (pSoldier->aiData.bAlertStatus != bOldStatus)
{
// HERE ARE TRYING TO AVOID NPCs SHUFFLING BACK & FORTH BETWEEN RED & BLACK
// if either status is < RED (ie. anything but RED->BLACK && BLACK->RED)
if ((bOldStatus < STATUS_RED) || (pSoldier->aiData.bAlertStatus < STATUS_RED))
{
// force a NEW action decision on next pass through HandleManAI()
SetNewSituation( pSoldier );
}
// if this guy JUST discovered that there were opponents here for sure...
if ((bOldStatus < STATUS_RED) && (pSoldier->aiData.bAlertStatus >= STATUS_RED))
{
// might want to make custom to let them go anywhere
CheckForChangingOrders(pSoldier);
}
#ifdef DEBUGDECISIONS
// don't report status changes for human-controlled mercs
// if (!pSoldier->human)
// {
STR16 tempstr;
sprintf(tempstr,"%s's Alert Status changed from %d to %d",
pSoldier->name ,bOldStatus,pSoldier->aiData.bAlertStatus);
AIPopMessage(tempstr);
// }
#endif
}
else // status didn't change
{
// if a guy on status GREEN or YELLOW is running low on breath
if (((pSoldier->aiData.bAlertStatus == STATUS_GREEN) && (pSoldier->bBreath < 75)) ||
((pSoldier->aiData.bAlertStatus == STATUS_YELLOW) && (pSoldier->bBreath < 50)))
{
// as long as he's not in water (standing on a bridge is OK)
if (!pSoldier->MercInWater())
{
// force a NEW decision so that he can get some rest
SetNewSituation( pSoldier );
// current action will be canceled. if noise is no longer important
if ((pSoldier->aiData.bAlertStatus == STATUS_YELLOW) &&
(TileIsOutOfBounds(MostImportantNoiseHeard(pSoldier,&iDummy,&fClimbDummy,&fReachableDummy))))
{
// then drop back to GREEN status
pSoldier->aiData.bAlertStatus = STATUS_GREEN;
CheckForChangingOrders(pSoldier);
}
}
}
}
}