Files
source/TacticalAI/AIUtils.cpp
T
lalien 1bab809f13 - always show game progress in version info (was only when using cheats)
- returned AI movement orders in MovementMode to old values (enemies were moving too slow, needs correction)


git-svn-id: https://ja2svn.mooo.com/source/ja2/trunk/GameSource/ja2_v1.13/Build@248 3b4a5df2-a311-0410-b5c6-a8a6f20db521
2006-06-17 11:14:21 +00:00

2521 lines
71 KiB
C++

#ifdef PRECOMPILEDHEADERS
#include "AI All.h"
#else
#include "ai.h"
#include "Weapons.h"
#include "opplist.h"
#include "Points.h"
#include "PathAI.h"
#include "WorldMan.h"
#include "AIInternals.h"
#include "Items.h"
#include "wcheck.h"
#include "message.h"
#include "los.h"
#include "assignments.h"
#include "Soldier Functions.h"
#include "Points.h"
#include "GameSettings.h"
#include "Buildings.h"
#endif
//
// CJC's DG->JA2 conversion notes
//
// Commented out:
//
// InWaterOrGas - gas stuff
// RoamingRange - point patrol stuff
extern UINT16 PickSoldierReadyAnimation( SOLDIERTYPE *pSoldier, BOOLEAN fEndReady );
UINT8 Urgency[NUM_STATUS_STATES][NUM_MORALE_STATES] =
{
{URGENCY_LOW, URGENCY_LOW, URGENCY_LOW, URGENCY_LOW, URGENCY_LOW}, // green
{URGENCY_HIGH, URGENCY_MED, URGENCY_MED, URGENCY_LOW, URGENCY_LOW}, // yellow
{URGENCY_HIGH, URGENCY_MED, URGENCY_MED, URGENCY_MED, URGENCY_MED}, // red
{URGENCY_HIGH, URGENCY_HIGH, URGENCY_HIGH, URGENCY_MED, URGENCY_MED} // black
};
UINT16 MovementMode[LAST_MOVEMENT_ACTION + 1][NUM_URGENCY_STATES] =
{
{WALKING, WALKING, WALKING }, // AI_ACTION_NONE
{WALKING, WALKING, WALKING }, // AI_ACTION_RANDOM_PATROL
{WALKING, RUNNING, RUNNING }, // AI_ACTION_SEEK_FRIEND
{WALKING, RUNNING, RUNNING }, // AI_ACTION_SEEK_OPPONENT
{RUNNING, RUNNING, RUNNING }, // AI_ACTION_TAKE_COVER
{WALKING, RUNNING, RUNNING }, // AI_ACTION_GET_CLOSER
{WALKING, WALKING, WALKING }, // AI_ACTION_POINT_PATROL,
{WALKING, RUNNING, RUNNING }, // AI_ACTION_LEAVE_WATER_GAS,
{WALKING, SWATTING, RUNNING }, // AI_ACTION_SEEK_NOISE,
{RUNNING, RUNNING, RUNNING }, // AI_ACTION_ESCORTED_MOVE,
{WALKING, RUNNING, RUNNING }, // AI_ACTION_RUN_AWAY,
{RUNNING, RUNNING, RUNNING }, // AI_ACTION_KNIFE_MOVE
{WALKING, WALKING, WALKING }, // AI_ACTION_APPROACH_MERC
{RUNNING, RUNNING, RUNNING }, // AI_ACTION_TRACK
{RUNNING, RUNNING, RUNNING }, // AI_ACTION_EAT
{WALKING, RUNNING, RUNNING}, // AI_ACTION_PICKUP_ITEM
{WALKING, WALKING, WALKING}, // AI_ACTION_SCHEDULE_MOVE
{WALKING, WALKING, WALKING}, // AI_ACTION_WALK
{WALKING, RUNNING, RUNNING}, // withdraw
{WALKING, SWATTING, SWATTING}, // flank left
{WALKING, SWATTING, SWATTING}, // flank right
{RUNNING, RUNNING, RUNNING}, // AI_ACTION_MOVE_TO_CLIMB
};
INT8 OKToAttack(SOLDIERTYPE * pSoldier, int target)
{
// can't shoot yourself
if (target == pSoldier->sGridNo)
return(NOSHOOT_MYSELF);
if (WaterTooDeepForAttacks(pSoldier->sGridNo))
return(NOSHOOT_WATER);
// make sure a weapon is in hand (FEB.8 ADDITION: tossable items are also OK)
if (!WeaponInHand(pSoldier))
{
return(NOSHOOT_NOWEAPON);
}
// JUST PUT THIS IN ON JULY 13 TO TRY AND FIX OUT-OF-AMMO SITUATIONS
if ( Item[pSoldier->inv[HANDPOS].usItem].usItemClass == IC_GUN)
{
if ( Item[pSoldier->inv[HANDPOS].usItem].cannon )
{
// look for another tank shell ELSEWHERE IN INVENTORY
if ( FindLaunchable( pSoldier, pSoldier->inv[HANDPOS].usItem ) == NO_SLOT )
//if ( !ItemHasAttachments( &(pSoldier->inv[HANDPOS]) ) )
{
return(NOSHOOT_NOLOAD);
}
}
else if (pSoldier->inv[HANDPOS].ubGunShotsLeft == 0 /*SB*/ || !(pSoldier->inv[HANDPOS].ubGunState & GS_CARTRIDGE_IN_CHAMBER))
{
return(NOSHOOT_NOAMMO);
}
}
else if (Item[pSoldier->inv[HANDPOS].usItem].usItemClass == IC_LAUNCHER)
{
if ( FindLaunchable( pSoldier, pSoldier->inv[HANDPOS].usItem ) == NO_SLOT )
//if ( !ItemHasAttachments( &(pSoldier->inv[HANDPOS]) ) )
{
return(NOSHOOT_NOLOAD);
}
}
return(TRUE);
}
BOOLEAN ConsiderProne( SOLDIERTYPE * pSoldier )
{
INT16 sOpponentGridNo;
INT8 bOpponentLevel;
INT32 iRange;
if (pSoldier->bAIMorale >= MORALE_NORMAL)
{
return( FALSE );
}
// We don't want to go prone if there is a nearby enemy
ClosestKnownOpponent( pSoldier, &sOpponentGridNo, &bOpponentLevel );
iRange = GetRangeFromGridNoDiff( pSoldier->sGridNo, sOpponentGridNo );
if (iRange > 10)
{
return( TRUE );
}
else
{
return( FALSE );
}
}
UINT8 StanceChange( SOLDIERTYPE * pSoldier, UINT8 ubAttackAPCost )
{
// consider crouching or going prone
if (PTR_STANDING)
{
if (pSoldier->bActionPoints - ubAttackAPCost >= AP_CROUCH)
{
if ( (pSoldier->bActionPoints - ubAttackAPCost >= AP_CROUCH + AP_PRONE) && IsValidStance( pSoldier, ANIM_PRONE ) && ConsiderProne( pSoldier ) )
{
return( ANIM_PRONE );
}
else if ( IsValidStance( pSoldier, ANIM_CROUCH ) )
{
return( ANIM_CROUCH );
}
}
}
else if (PTR_CROUCHED)
{
if ( (pSoldier->bActionPoints - ubAttackAPCost >= AP_PRONE) && IsValidStance( pSoldier, ANIM_PRONE ) && ConsiderProne( pSoldier ) )
{
return( ANIM_PRONE );
}
}
return( 0 );
}
UINT8 ShootingStanceChange( SOLDIERTYPE * pSoldier, ATTACKTYPE * pAttack, INT8 bDesiredDirection )
{
// Figure out the best stance for this attack
// We don't want to go through a lot of complex calculations here,
// just compare the chance of the bullet hitting if we are
// standing, crouched, or prone
UINT16 usRealAnimState, usBestAnimState;
INT8 bBestStanceDiff=-1;
INT8 bLoop, bStanceNum, bStanceDiff, bAPsAfterAttack;
UINT32 uiChanceOfDamage, uiBestChanceOfDamage, uiCurrChanceOfDamage;
UINT32 uiStanceBonus, uiMinimumStanceBonusPerChange = 20 - 3 * pAttack->ubAimTime;
INT32 iRange;
bStanceNum = 0;
uiCurrChanceOfDamage = 0;
bAPsAfterAttack = pSoldier->bActionPoints - pAttack->ubAPCost - GetAPsToReadyWeapon( pSoldier, pSoldier->usAnimState );
if (bAPsAfterAttack < AP_CROUCH)
{
return( 0 );
}
// Unfortunately, to get this to work, we have to fake the AI guy's
// animation state so we get the right height values
usRealAnimState = pSoldier->usAnimState;
usBestAnimState = pSoldier->usAnimState;
uiBestChanceOfDamage = 0;
iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, pAttack->sTarget );
switch( gAnimControl[usRealAnimState].ubEndHeight )
{
// set a stance number comparable with our loop variable so we can easily compute
// stance differences and thus AP cost
case ANIM_STAND:
bStanceNum = 0;
break;
case ANIM_CROUCH:
bStanceNum = 1;
break;
case ANIM_PRONE:
bStanceNum = 2;
break;
}
for (bLoop = 0; bLoop < 3; bLoop++)
{
bStanceDiff = abs( bLoop - bStanceNum );
if (bStanceDiff == 2 && bAPsAfterAttack < AP_CROUCH + AP_PRONE)
{
// can't consider this!
continue;
}
switch( bLoop )
{
case 0:
if ( !InternalIsValidStance( pSoldier, bDesiredDirection, ANIM_STAND ) )
{
continue;
}
pSoldier->usAnimState = STANDING;
break;
case 1:
if ( !InternalIsValidStance( pSoldier, bDesiredDirection, ANIM_CROUCH ) )
{
continue;
}
pSoldier->usAnimState = CROUCHING;
break;
default:
if ( !InternalIsValidStance( pSoldier, bDesiredDirection, ANIM_PRONE ) )
{
continue;
}
pSoldier->usAnimState = PRONE;
break;
}
uiChanceOfDamage = SoldierToLocationChanceToGetThrough( pSoldier, pAttack->sTarget, pSoldier->bTargetLevel, pSoldier->bTargetCubeLevel, pAttack->ubOpponent ) * CalcChanceToHitGun( pSoldier, pAttack->sTarget, pAttack->ubAimTime, AIM_SHOT_TORSO ) / 100;
if (uiChanceOfDamage > 0)
{
uiStanceBonus = 0;
// artificially augment "chance of damage" to reflect penalty to be shot at various stances
switch( pSoldier->usAnimState )
{
case CROUCHING:
if (iRange > POINT_BLANK_RANGE + 10 * (AIM_PENALTY_TARGET_CROUCHED / 3))
{
uiStanceBonus = AIM_BONUS_CROUCHING;
}
else if (iRange > POINT_BLANK_RANGE)
{
// reduce chance to hit with distance to the prone/immersed target
uiStanceBonus = 3 * ((iRange - POINT_BLANK_RANGE) / CELL_X_SIZE); // penalty -3%/tile
}
break;
case PRONE:
if (iRange <= MIN_PRONE_RANGE)
{
// HATE being prone this close!
uiChanceOfDamage = 0;
}
else //if (iRange > POINT_BLANK_RANGE)
{
// reduce chance to hit with distance to the prone/immersed target
uiStanceBonus = 3 * ((iRange - POINT_BLANK_RANGE) / CELL_X_SIZE); // penalty -3%/tile
}
break;
default:
break;
}
// reduce stance bonus according to how much we have to change stance to get there
//uiStanceBonus = uiStanceBonus * (4 - bStanceDiff) / 4;
uiChanceOfDamage += uiStanceBonus;
}
if (bStanceDiff == 0)
{
uiCurrChanceOfDamage = uiChanceOfDamage;
}
if (uiChanceOfDamage > uiBestChanceOfDamage )
{
uiBestChanceOfDamage = uiChanceOfDamage;
usBestAnimState = pSoldier->usAnimState;
bBestStanceDiff = bStanceDiff;
}
}
pSoldier->usAnimState = usRealAnimState;
// return 0 or the best height value to be at
if (bBestStanceDiff == 0 || ((uiBestChanceOfDamage - uiCurrChanceOfDamage) / bBestStanceDiff) < uiMinimumStanceBonusPerChange)
{
// better off not changing our stance!
return( 0 );
}
else
{
return( gAnimControl[ usBestAnimState ].ubEndHeight );
}
}
UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction )
{
if ( pSoldier->fUIMovementFast )
{
return( RUNNING );
}
else if ( CREATURE_OR_BLOODCAT( pSoldier ) )
{
if (pSoldier->bAlertStatus == STATUS_GREEN)
{
return( WALKING );
}
else
{
return( RUNNING );
}
}
else if (pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CROW)
{
return( WALKING );
}
else
{
if ( (pSoldier->fAIFlags & AI_CAUTIOUS) && (MovementMode[bAction][Urgency[pSoldier->bAlertStatus][pSoldier->bAIMorale]] == RUNNING) )
{
return( WALKING );
}
else if ( bAction == AI_ACTION_SEEK_NOISE && pSoldier->bTeam == CIV_TEAM && !IS_MERC_BODY_TYPE( pSoldier ) )
{
return( WALKING );
}
else if ( (pSoldier->ubBodyType == HATKIDCIV || pSoldier->ubBodyType == KIDCIV) && (pSoldier->bAlertStatus == STATUS_GREEN) && Random( 10 ) == 0 )
{
return( KID_SKIPPING );
}
else
{
return( MovementMode[bAction][Urgency[pSoldier->bAlertStatus][pSoldier->bAIMorale]] );
}
}
}
void NewDest(SOLDIERTYPE *pSoldier, UINT16 usGridNo)
{
// ATE: Setting sDestination? Tis does not make sense...
//pSoldier->sDestination = usGridNo;
BOOLEAN fSet = FALSE;
if ( IS_MERC_BODY_TYPE( pSoldier ) && pSoldier->bAction == AI_ACTION_TAKE_COVER && (pSoldier->bOrders == DEFENSIVE || pSoldier->bOrders == CUNNINGSOLO || pSoldier->bOrders == CUNNINGAID ) && (SoldierDifficultyLevel( pSoldier ) >= 2) )
{
UINT16 usMovementMode;
// getting real movement anim for someone who is going to take cover, not just considering
usMovementMode = MovementMode[AI_ACTION_TAKE_COVER][Urgency[pSoldier->bAlertStatus][pSoldier->bAIMorale]];
if ( usMovementMode != SWATTING )
{
// really want to look at path, see how far we could get on path while swatting
if ( EnoughPoints( pSoldier, RecalculatePathCost( pSoldier, SWATTING ), 0, FALSE ) || (pSoldier->bLastAction == AI_ACTION_TAKE_COVER && pSoldier->usUIMovementMode == SWATTING ) )
{
pSoldier->usUIMovementMode = SWATTING;
}
else
{
pSoldier->usUIMovementMode = usMovementMode;
}
}
else
{
pSoldier->usUIMovementMode = usMovementMode;
}
fSet = TRUE;
}
else
{
if ( pSoldier->bTeam == ENEMY_TEAM && pSoldier->bAlertStatus == STATUS_RED )
{
switch( pSoldier->bAction )
{
case AI_ACTION_MOVE_TO_CLIMB:
case AI_ACTION_RUN_AWAY:
pSoldier->usUIMovementMode = DetermineMovementMode( pSoldier, pSoldier->bAction );
fSet = TRUE;
break;
default:
/* if ( PreRandom( 5 - SoldierDifficultyLevel( pSoldier ) ) == 0 )
{
INT16 sClosestNoise = (INT16) MostImportantNoiseHeard( pSoldier, NULL, NULL, NULL );
if ( sClosestNoise != NOWHERE && PythSpacesAway( pSoldier->sGridNo, sClosestNoise ) < MaxDistanceVisible() + 10 )
{
pSoldier->usUIMovementMode = SWATTING;
fSet = TRUE;
}
}*/
if ( !fSet )
{
pSoldier->usUIMovementMode = DetermineMovementMode( pSoldier, pSoldier->bAction );
fSet = TRUE;
}
break;
}
}
else
{
pSoldier->usUIMovementMode = DetermineMovementMode( pSoldier, pSoldier->bAction );
fSet = TRUE;
}
if ( pSoldier->usUIMovementMode == SWATTING && !IS_MERC_BODY_TYPE( pSoldier ) )
{
pSoldier->usUIMovementMode = WALKING;
}
}
//EVENT_GetNewSoldierPath( pSoldier, pSoldier->sDestination, pSoldier->usUIMovementMode );
// ATE: Using this more versitile version
// Last paramater says whether to re-start the soldier's animation
// This should be done if buddy was paused for fNoApstofinishMove...
EVENT_InternalGetNewSoldierPath( pSoldier, usGridNo, pSoldier->usUIMovementMode , FALSE, pSoldier->fNoAPToFinishMove );
}
BOOLEAN IsActionAffordable(SOLDIERTYPE *pSoldier)
{
INT8 bMinPointsNeeded = 0;
INT8 bAPForStandUp = 0;
INT8 bAPToLookAtWall = ( FindDirectionForClimbing( pSoldier->sGridNo ) == pSoldier->bDirection ) ? 0 : 1;
//NumMessage("AffordableAction - Guy#",pSoldier->ubID);
switch (pSoldier->bAction)
{
case AI_ACTION_NONE: // maintain current position & facing
// no cost for doing nothing!
break;
case AI_ACTION_CHANGE_FACING: // turn to face another direction
bMinPointsNeeded = (INT8) GetAPsToLook( pSoldier );
break;
case AI_ACTION_RANDOM_PATROL: // move towards a particular location
case AI_ACTION_SEEK_FRIEND: // move towards friend in trouble
case AI_ACTION_SEEK_OPPONENT: // move towards a reported opponent
case AI_ACTION_TAKE_COVER: // run for nearest cover from threat
case AI_ACTION_GET_CLOSER: // move closer to a strategic location
case AI_ACTION_POINT_PATROL: // move towards next patrol point
case AI_ACTION_LEAVE_WATER_GAS: // seek nearest spot of ungassed land
case AI_ACTION_SEEK_NOISE: // seek most important noise heard
case AI_ACTION_ESCORTED_MOVE: // go where told to by escortPlayer
case AI_ACTION_RUN_AWAY: // run away from nearby opponent(s)
case AI_ACTION_APPROACH_MERC:
case AI_ACTION_TRACK:
case AI_ACTION_EAT:
case AI_ACTION_SCHEDULE_MOVE:
case AI_ACTION_WALK:
case AI_ACTION_MOVE_TO_CLIMB:
// for movement, must have enough APs to move at least 1 tile's worth
bMinPointsNeeded = MinPtsToMove(pSoldier);
break;
case AI_ACTION_PICKUP_ITEM: // grab things lying on the ground
bMinPointsNeeded = __max( MinPtsToMove( pSoldier ), AP_PICKUP_ITEM );
break;
case AI_ACTION_OPEN_OR_CLOSE_DOOR:
case AI_ACTION_UNLOCK_DOOR:
case AI_ACTION_LOCK_DOOR:
bMinPointsNeeded = MinPtsToMove(pSoldier);
break;
case AI_ACTION_DROP_ITEM:
bMinPointsNeeded = AP_PICKUP_ITEM;
break;
case AI_ACTION_FIRE_GUN: // shoot at nearby opponent
case AI_ACTION_TOSS_PROJECTILE: // throw grenade at/near opponent(s)
case AI_ACTION_KNIFE_MOVE: // preparing to stab adjacent opponent
case AI_ACTION_THROW_KNIFE:
// only FIRE_GUN currently actually pays extra turning costs!
bMinPointsNeeded = MinAPsToAttack(pSoldier,pSoldier->usActionData,ADDTURNCOST);
#ifdef BETAVERSION
if (ptsNeeded > pSoldier->bActionPoints)
{
/*
sprintf((CHAR *)tempstr,"AI ERROR: %s has insufficient points for attack action %d at grid %d",
pSoldier->name,pSoldier->bAction,pSoldier->usActionData);
PopMessage(tempstr);
*/
}
#endif
break;
case AI_ACTION_PULL_TRIGGER: // activate an adjacent panic trigger
bMinPointsNeeded = AP_PULL_TRIGGER;
break;
case AI_ACTION_USE_DETONATOR: // grab detonator and set off bomb(s)
bMinPointsNeeded = AP_USE_REMOTE;
break;
case AI_ACTION_YELLOW_ALERT: // tell friends opponent(s) heard
case AI_ACTION_RED_ALERT: // tell friends opponent(s) seen
case AI_ACTION_CREATURE_CALL: // for now
bMinPointsNeeded = AP_RADIO;
break;
case AI_ACTION_CHANGE_STANCE: // crouch
bMinPointsNeeded = AP_CROUCH;
break;
case AI_ACTION_GIVE_AID: // help injured/dying friend
bMinPointsNeeded = 0;
break;
case AI_ACTION_CLIMB_ROOF:
if (pSoldier->bLevel == 0)
{
if( PTR_CROUCHED ) bAPForStandUp = 2;
if( PTR_PRONE ) bAPForStandUp = 4;
bMinPointsNeeded = AP_CLIMBROOF + bAPForStandUp + bAPToLookAtWall;
}
else
{
if( !PTR_CROUCHED ) bAPForStandUp = 2;
bMinPointsNeeded = AP_CLIMBOFFROOF + bAPForStandUp + bAPToLookAtWall;
}
break;
case AI_ACTION_COWER:
case AI_ACTION_STOP_COWERING:
case AI_ACTION_LOWER_GUN:
case AI_ACTION_END_COWER_AND_MOVE:
case AI_ACTION_TRAVERSE_DOWN:
case AI_ACTION_OFFER_SURRENDER:
bMinPointsNeeded = 0;
break;
default:
#ifdef BETAVERSION
//NumMessage("AffordableAction - Illegal action type = ",pSoldier->bAction);
#endif
break;
}
// check whether or not we can afford to do this action
if (bMinPointsNeeded > pSoldier->bActionPoints)
{
return(FALSE);
}
else
{
return(TRUE);
}
}
INT16 RandomFriendWithin(SOLDIERTYPE *pSoldier)
{
UINT32 uiLoop;
UINT16 usMaxDist;
UINT8 ubFriendCount, ubFriendIDs[MAXMERCS], ubFriendID;
UINT16 usDirection;
UINT8 ubDirsLeft;
BOOLEAN fDirChecked[8];
BOOLEAN fRangeRestricted = FALSE, fFound = FALSE;
UINT16 usDest, usOrigin;
SOLDIERTYPE * pFriend;
// obtain maximum roaming distance from soldier's origin
usMaxDist = RoamingRange(pSoldier, (INT16 *)&usOrigin);
// if our movement range is restricted
// CJC: since RandomFriendWithin is only used in non-combat, ALWAYS restrict range.
fRangeRestricted = TRUE;
/*
if (usMaxDist < MAX_ROAMING_RANGE)
{
fRangeRestricted = TRUE;
}
*/
// if range is restricted, make sure origin is a legal gridno!
if (fRangeRestricted && ((usOrigin < 0) || (usOrigin >= GRIDSIZE)))
{
#ifdef BETAVERSION
NameMessage(pSoldier,"has illegal origin, but his roaming range is restricted!",1000);
#endif
return(FALSE);
}
ubFriendCount = 0;
// build a list of the guynums of all active, eligible friendly mercs
// go through each soldier, looking for "friends" (soldiers on same side)
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
{
pFriend = MercSlots[ uiLoop ];
// if this merc is inactive, not in sector, or dead
if (!pFriend)
{
continue;
}
// skip ourselves
if (pFriend->ubID == pSoldier->ubID)
{
continue;
}
// if this man not neutral, but is on my side, OR if he is neutral, but
// so am I, then he's a "friend" for the purposes of random visitations
if ((!pFriend->bNeutral && (pSoldier->bSide == pFriend->bSide)) ||
(pFriend->bNeutral && pSoldier->bNeutral))
{
// if we're not already neighbors
if (SpacesAway(pSoldier->sGridNo,pFriend->sGridNo) > 1)
{
// remember his guynum, increment friend counter
ubFriendIDs[ubFriendCount++] = pFriend->ubID;
}
}
}
while (ubFriendCount && !fFound)
{
// randomly select one of the remaining friends in the list
ubFriendID = ubFriendIDs[PreRandom(ubFriendCount)];
// if our movement range is NOT restricted, or this friend's within range
// use distance - 1, because there must be at least 1 tile 1 space closer
if (!fRangeRestricted ||
(SpacesAway(usOrigin,Menptr[ubFriendID].sGridNo) - 1) <= usMaxDist)
{
// should be close enough, try to find a legal ->sDestination within 1 tile
// clear dirChecked flag for all 8 directions
for (usDirection = 0; usDirection < 8; usDirection++)
{
fDirChecked[usDirection] = FALSE;
}
ubDirsLeft = 8;
// examine all 8 spots around 'ubFriendID'
// keep looking while directions remain and a satisfactory one not found
while ((ubDirsLeft--) && !fFound)
{
// randomly select a direction which hasn't been 'checked' yet
do
{
usDirection = (UINT16) Random(8);
}
while (fDirChecked[usDirection]);
fDirChecked[usDirection] = TRUE;
// determine the gridno 1 tile away from current friend in this direction
usDest = NewGridNo(Menptr[ubFriendID].sGridNo,DirectionInc((INT16)(usDirection + 1)));
// if that's out of bounds, ignore it & check next direction
if (usDest == Menptr[ubFriendID].sGridNo)
{
continue;
}
// if our movement range is NOT restricted
if (!fRangeRestricted || (SpacesAway(usOrigin,usDest) <= usMaxDist))
{
if (LegalNPCDestination(pSoldier,usDest,ENSURE_PATH,NOWATER, 0))
{
fFound = TRUE; // found a spot
pSoldier->usActionData = usDest; // store this ->sDestination
pSoldier->bPathStored = TRUE; // optimization - Ian
break; // stop checking in other directions
}
}
}
}
if (!fFound)
{
ubFriendCount--;
// if we hadn't already picked the last friend currently in the list
if (ubFriendCount != ubFriendID)
{
ubFriendIDs[ubFriendID] = ubFriendIDs[ubFriendCount];
}
}
}
return(fFound);
}
INT16 RandDestWithinRange(SOLDIERTYPE *pSoldier)
{
INT16 sRandDest = NOWHERE;
UINT16 usOrigin, usMaxDist;
UINT8 ubTriesLeft;
BOOLEAN fLimited = FALSE, fFound = FALSE;
INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXRange, sYRange, sXOffset, sYOffset;
INT16 sOrigX, sOrigY;
INT16 sX, sY;
UINT8 ubRoom = 0, ubTempRoom;
sOrigX = sOrigY = -1;
sMaxLeft = sMaxRight = sMaxUp = sMaxDown = sXRange = sYRange = -1;
// Try to find a random ->sDestination that's no more than maxDist away from
// the given gridno of origin
if (gfTurnBasedAI)
{
ubTriesLeft = 10;
}
else
{
ubTriesLeft = 1;
}
usMaxDist = RoamingRange(pSoldier, (INT16 *)&usOrigin);
if ( pSoldier->bOrders <= CLOSEPATROL && (pSoldier->bTeam == CIV_TEAM || pSoldier->ubProfile != NO_PROFILE ) )
{
// any other combo uses the default of ubRoom == 0, set above
if ( !InARoom( pSoldier->usPatrolGrid[0], &ubRoom ) )
{
ubRoom = 0;
}
}
// if the maxDist is truly a restriction
if (usMaxDist < (MAXCOL - 1))
{
fLimited = TRUE;
// determine maximum horizontal limits
sOrigX = usOrigin % MAXCOL;
sOrigY = usOrigin / MAXCOL;
sMaxLeft = min(usMaxDist, sOrigX);
sMaxRight = min(usMaxDist,MAXCOL - (sOrigX + 1));
// determine maximum vertical limits
sMaxUp = min(usMaxDist, sOrigY);
sMaxDown = min(usMaxDist,MAXROW - (sOrigY + 1));
sXRange = sMaxLeft + sMaxRight + 1;
sYRange = sMaxUp + sMaxDown + 1;
}
if (pSoldier->ubBodyType == LARVAE_MONSTER)
{
// only crawl 1 tile, within our roaming range
while ((ubTriesLeft--) && !fFound)
{
sXOffset = (INT16) Random( 3 ) - 1; // generates -1 to +1
sYOffset = (INT16) Random( 3 ) - 1;
if (fLimited)
{
sX = pSoldier->sGridNo % MAXCOL + sXOffset;
sY = pSoldier->sGridNo / MAXCOL + sYOffset;
if (sX < sOrigX - sMaxLeft || sX > sOrigX + sMaxRight)
{
continue;
}
if (sY < sOrigY - sMaxUp || sY > sOrigY + sMaxDown)
{
continue;
}
sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset);
}
else
{
sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset);
}
if (!LegalNPCDestination(pSoldier,sRandDest,ENSURE_PATH,NOWATER,0))
{
sRandDest = NOWHERE;
continue; // try again!
}
// passed all the tests, ->sDestination is acceptable
fFound = TRUE;
pSoldier->bPathStored = TRUE; // optimization - Ian
}
}
else
{
// keep rolling random ->sDestinations until one's satisfactory or retries used
while ((ubTriesLeft--) && !fFound)
{
if (fLimited)
{
sXOffset = ((INT16)Random(sXRange)) - sMaxLeft;
sYOffset = ((INT16)Random(sYRange)) - sMaxUp;
sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset);
#ifdef BETAVERSION
if ((sRandDest < 0) || (sRandDest >= GRIDSIZE))
{
NumMessage("RandDestWithinRange: ERROR - Gridno out of range! = ",sRandDest);
sRandDest = random(GRIDSIZE);
}
#endif
}
else
{
sRandDest = (INT16) PreRandom(GRIDSIZE);
}
if ( ubRoom && InARoom( sRandDest, &ubTempRoom ) && ubTempRoom != ubRoom )
{
// outside of room available for patrol!
sRandDest = NOWHERE;
continue;
}
if (!LegalNPCDestination(pSoldier,sRandDest,ENSURE_PATH,NOWATER,0))
{
sRandDest = NOWHERE;
continue; // try again!
}
// passed all the tests, ->sDestination is acceptable
fFound = TRUE;
pSoldier->bPathStored = TRUE; // optimization - Ian
}
}
return(sRandDest); // defaults to NOWHERE
}
INT16 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK, BOOLEAN * pfChangeLevel )
{
INT16 *psLastLoc, *pusNoiseGridNo;
INT8 *pbLastLevel;
INT16 sGridNo=-1;
INT8 bLevel, bClosestLevel;
BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE;
INT32 iPathCost;
INT16 sClosestDisturbance = NOWHERE;
UINT32 uiLoop;
UINT16 closestConscious = NOWHERE,closestUnconscious = NOWHERE;
INT32 iShortestPath = 1000;
INT32 iShortestPathConscious = 1000,iShortestPathUnconscious = 1000;
UINT8 *pubNoiseVolume;
INT8 *pbNoiseLevel;
INT8 *pbPersOL,*pbPublOL;
INT16 sClimbGridNo;
SOLDIERTYPE * pOpp;
// CJC: can't trace a path to every known disturbance!
// for starters, try the closest one as the crow flies
INT16 sClosestEnemy = NOWHERE, sDistToClosestEnemy = 1000, sDistToEnemy;
*pfChangeLevel = FALSE;
pubNoiseVolume = &gubPublicNoiseVolume[pSoldier->bTeam];
pusNoiseGridNo = &gsPublicNoiseGridno[pSoldier->bTeam];
pbNoiseLevel = &gbPublicNoiseLevel[pSoldier->bTeam];
// hang pointers at start of this guy's personal and public opponent opplists
// pbPersOL = &pSoldier->bOppList[0];
// pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]);
// psLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]);
// look through this man's personal & public opplists for opponents known
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
{
pOpp = MercSlots[ uiLoop ];
// if this merc is inactive, at base, on assignment, or dead
if (!pOpp)
{
continue; // next merc
}
// if this merc is neutral/on same side, he's not an opponent
if ( CONSIDERED_NEUTRAL( pSoldier, pOpp ) || (pSoldier->bSide == pOpp->bSide) )
{
continue; // next merc
}
pbPersOL = pSoldier->bOppList + pOpp->ubID;
pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpp->ubID;
psLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpp->ubID;
pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpp->ubID;
// if this opponent is unknown personally and publicly
if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
{
continue; // next merc
}
// this is possible if get here from BLACK AI in one of those rare
// instances when we couldn't get a meaningful shot off at a guy in sight
if ((*pbPersOL == SEEN_CURRENTLY) && (pOpp->bLife >= OKLIFE))
{
// don't allow this to return any valid values, this guy remains a
// serious threat and the last thing we want to do is approach him!
return(NOWHERE);
}
// if personal knowledge is more up to date or at least equal
if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) ||
(*pbPersOL == *pbPublOL))
{
// using personal knowledge, obtain opponent's "best guess" gridno
sGridNo = *psLastLoc;
bLevel = *pbLastLevel;
}
else
{
// using public knowledge, obtain opponent's "best guess" gridno
sGridNo = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpp->ubID];
bLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpp->ubID];
}
// if we are standing at that gridno (!, obviously our info is old...)
if (sGridNo == pSoldier->sGridNo)
{
continue; // next merc
}
if (sGridNo == NOWHERE)
{
// huh?
continue;
}
sDistToEnemy = PythSpacesAway( pSoldier->sGridNo, sGridNo );
if (sDistToEnemy < sDistToClosestEnemy )
{
sClosestEnemy = sGridNo;
bClosestLevel = bLevel;
sDistToClosestEnemy = sDistToEnemy;
}
}
if (sClosestEnemy != NOWHERE)
{
iPathCost = EstimatePathCostToLocation( pSoldier, sClosestEnemy, bClosestLevel, FALSE, &fClimbingNecessary, &sClimbGridNo );
// if we can get there
if (iPathCost != 0)
{
if (fClimbingNecessary)
{
sClosestDisturbance = sClimbGridNo;
}
else
{
sClosestDisturbance = sClosestEnemy;
}
iShortestPath = iPathCost;
fClosestClimbingNecessary = fClimbingNecessary;
}
}
// if any "misc. noise" was also heard recently
if (pSoldier->sNoiseGridno != NOWHERE && pSoldier->sNoiseGridno != sClosestDisturbance)
{
// test this gridno, too
sGridNo = pSoldier->sNoiseGridno;
bLevel = pSoldier->bNoiseLevel;
// if we are there (at the noise gridno)
if (sGridNo == pSoldier->sGridNo)
{
pSoldier->sNoiseGridno = NOWHERE; // wipe it out, not useful anymore
pSoldier->ubNoiseVolume = 0;
}
else
{
// get the AP cost to get to the location of the noise
iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo );
// if we can get there
if (iPathCost != 0)
{
if (fClimbingNecessary)
{
sClosestDisturbance = sClimbGridNo;
}
else
{
sClosestDisturbance = sGridNo;
}
iShortestPath = iPathCost;
fClosestClimbingNecessary = fClimbingNecessary;
}
}
}
// if any PUBLIC "misc. noise" was also heard recently
if (*pusNoiseGridNo != NOWHERE && *pusNoiseGridNo != sClosestDisturbance )
{
// test this gridno, too
sGridNo = *pusNoiseGridNo;
bLevel = *pbNoiseLevel;
// if we are not NEAR the noise gridno...
if ( pSoldier->bLevel != bLevel || PythSpacesAway( pSoldier->sGridNo, sGridNo ) >= 6 || SoldierTo3DLocationLineOfSightTest( pSoldier, sGridNo, bLevel, 0, (UINT8) MaxDistanceVisible(), FALSE ) == 0 )
// if we are NOT there (at the noise gridno)
// if (sGridNo != pSoldier->sGridNo)
{
// get the AP cost to get to the location of the noise
iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo );
// if we can get there
if (iPathCost != 0)
{
if (fClimbingNecessary)
{
sClosestDisturbance = sClimbGridNo;
}
else
{
sClosestDisturbance = sGridNo;
}
iShortestPath = iPathCost;
fClosestClimbingNecessary = fClimbingNecessary;
}
}
else
{
// degrade our public noise a bit
*pusNoiseGridNo -= 2;
}
}
#ifdef DEBUGDECISIONS
if (sClosestDisturbance != NOWHERE)
{
AINumMessage("CLOSEST DISTURBANCE is at gridno ",sClosestDisturbance);
}
#endif
*pfChangeLevel = fClosestClimbingNecessary;
return(sClosestDisturbance);
}
INT16 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT16 * psGridNo, INT8 * pbLevel)
{
INT16 *psLastLoc,sGridNo, sClosestOpponent = NOWHERE;
UINT32 uiLoop;
INT32 iRange, iClosestRange = 1500;
INT8 *pbPersOL,*pbPublOL;
INT8 bLevel, bClosestLevel;
SOLDIERTYPE * pOpp;
bClosestLevel = -1;
// NOTE: THIS FUNCTION ALLOWS RETURN OF UNCONSCIOUS AND UNREACHABLE OPPONENTS
psLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]);
// hang pointers at start of this guy's personal and public opponent opplists
pbPersOL = &pSoldier->bOppList[0];
pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]);
// look through this man's personal & public opplists for opponents known
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
{
pOpp = MercSlots[ uiLoop ];
// if this merc is inactive, at base, on assignment, or dead
if (!pOpp)
{
continue; // next merc
}
// if this merc is neutral/on same side, he's not an opponent
if ( CONSIDERED_NEUTRAL( pSoldier, pOpp ) || (pSoldier->bSide == pOpp->bSide))
{
continue; // next merc
}
// Special stuff for Carmen the bounty hunter
if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpp->ubProfile != 64)
{
continue; // next opponent
}
pbPersOL = pSoldier->bOppList + pOpp->ubID;
pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpp->ubID;
psLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpp->ubID;
// if this opponent is unknown personally and publicly
if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
{
continue; // next merc
}
// if personal knowledge is more up to date or at least equal
if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) ||
(*pbPersOL == *pbPublOL))
{
// using personal knowledge, obtain opponent's "best guess" gridno
sGridNo = gsLastKnownOppLoc[pSoldier->ubID][pOpp->ubID];
bLevel = gbLastKnownOppLevel[pSoldier->ubID][pOpp->ubID];
}
else
{
// using public knowledge, obtain opponent's "best guess" gridno
sGridNo = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpp->ubID];
bLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpp->ubID];
}
// if we are standing at that gridno(!, obviously our info is old...)
if (sGridNo == pSoldier->sGridNo)
{
continue; // next merc
}
// this function is used only for turning towards closest opponent or changing stance
// as such, if they AI is in a building,
// we should ignore people who are on the roof of the same building as the AI
if ( (bLevel != pSoldier->bLevel) && SameBuilding( pSoldier->sGridNo, sGridNo ) )
{
continue;
}
// I hope this will be good enough; otherwise we need a fractional/world-units-based 2D distance function
//sRange = PythSpacesAway( pSoldier->sGridNo, sGridNo);
iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo );
if (iRange < iClosestRange)
{
iClosestRange = iRange;
sClosestOpponent = sGridNo;
bClosestLevel = bLevel;
}
}
#ifdef DEBUGDECISIONS
if (sClosestOpponent != NOWHERE)
{
AINumMessage("CLOSEST OPPONENT is at gridno ",sClosestOpponent);
}
#endif
if (psGridNo)
{
*psGridNo = sClosestOpponent;
}
if (pbLevel)
{
*pbLevel = bClosestLevel;
}
return( sClosestOpponent );
}
INT16 ClosestSeenOpponent(SOLDIERTYPE *pSoldier, INT16 * psGridNo, INT8 * pbLevel)
{
INT16 sGridNo, sClosestOpponent = NOWHERE;
UINT32 uiLoop;
INT32 iRange, iClosestRange = 1500;
INT8 *pbPersOL;
INT8 bLevel, bClosestLevel;
SOLDIERTYPE * pOpp;
bClosestLevel = -1;
// look through this man's personal & public opplists for opponents known
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
{
pOpp = MercSlots[ uiLoop ];
// if this merc is inactive, at base, on assignment, or dead
if (!pOpp)
{
continue; // next merc
}
// if this merc is neutral/on same side, he's not an opponent
if ( CONSIDERED_NEUTRAL( pSoldier, pOpp ) || (pSoldier->bSide == pOpp->bSide))
{
continue; // next merc
}
// Special stuff for Carmen the bounty hunter
if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpp->ubProfile != 64)
{
continue; // next opponent
}
pbPersOL = pSoldier->bOppList + pOpp->ubID;
// if this opponent is not seen personally
if (*pbPersOL != SEEN_CURRENTLY)
{
continue; // next merc
}
// since we're dealing with seen people, use exact gridnos
sGridNo = pOpp->sGridNo;
bLevel = pOpp->bLevel;
// if we are standing at that gridno(!, obviously our info is old...)
if (sGridNo == pSoldier->sGridNo)
{
continue; // next merc
}
// this function is used only for turning towards closest opponent or changing stance
// as such, if they AI is in a building,
// we should ignore people who are on the roof of the same building as the AI
if ( (bLevel != pSoldier->bLevel) && SameBuilding( pSoldier->sGridNo, sGridNo ) )
{
continue;
}
// I hope this will be good enough; otherwise we need a fractional/world-units-based 2D distance function
//sRange = PythSpacesAway( pSoldier->sGridNo, sGridNo);
iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo );
if (iRange < iClosestRange)
{
iClosestRange = iRange;
sClosestOpponent = sGridNo;
bClosestLevel = bLevel;
}
}
#ifdef DEBUGDECISIONS
if (sClosestOpponent != NOWHERE)
{
AINumMessage("CLOSEST OPPONENT is at gridno ",sClosestOpponent);
}
#endif
if (psGridNo)
{
*psGridNo = sClosestOpponent;
}
if (pbLevel)
{
*pbLevel = bClosestLevel;
}
return( sClosestOpponent );
}
INT16 ClosestPC( SOLDIERTYPE *pSoldier, INT16 * psDistance )
{
// used by NPCs... find the closest PC
// NOTE: skips EPCs!
UINT8 ubLoop;
SOLDIERTYPE *pTargetSoldier;
INT16 sMinDist = (INT16)WORLD_MAX;
INT16 sDist;
INT16 sGridNo = NOWHERE;
// Loop through all mercs on player team
ubLoop = gTacticalStatus.Team[ gbPlayerNum ].bFirstID;
for ( ; ubLoop <= gTacticalStatus.Team[ gbPlayerNum ].bLastID; ubLoop++)
{
pTargetSoldier = Menptr + ubLoop;
if (!pTargetSoldier->bActive || !pTargetSoldier->bInSector)
{
continue;
}
// if not conscious, skip him
if (pTargetSoldier->bLife < OKLIFE)
{
continue;
}
if ( AM_AN_EPC( pTargetSoldier ) )
{
continue;
}
sDist = PythSpacesAway(pSoldier->sGridNo,pTargetSoldier->sGridNo);
// if this PC is not visible to the soldier, then add a penalty to the distance
// so that we weight in favour of visible mercs
if ( pTargetSoldier->bTeam != pSoldier->bTeam && pSoldier->bOppList[ ubLoop ] != SEEN_CURRENTLY )
{
sDist += 10;
}
if (sDist < sMinDist)
{
sMinDist = sDist;
sGridNo = pTargetSoldier->sGridNo;
}
}
if ( psDistance )
{
*psDistance = sMinDist;
}
return( sGridNo );
}
INT16 FindClosestClimbPointAvailableToAI( SOLDIERTYPE * pSoldier, INT16 sStartGridNo, INT16 sDesiredGridNo, BOOLEAN fClimbUp )
{
INT16 sGridNo;
INT16 sRoamingOrigin;
INT16 sRoamingRange;
if ( pSoldier->uiStatusFlags & SOLDIER_PC )
{
sRoamingOrigin = pSoldier->sGridNo;
sRoamingRange = 99;
}
else
{
sRoamingRange = RoamingRange( pSoldier, &sRoamingOrigin );
}
// since climbing necessary involves going an extra tile, we compare against 1 less than the roam range...
// or add 1 to the distance to the climb point
sGridNo = FindClosestClimbPoint( sStartGridNo, sDesiredGridNo, fClimbUp );
if ( PythSpacesAway( sRoamingOrigin, sGridNo ) + 1 > sRoamingRange )
{
return( NOWHERE );
}
else
{
return( sGridNo );
}
}
BOOLEAN ClimbingNecessary( SOLDIERTYPE * pSoldier, INT16 sDestGridNo, INT8 bDestLevel )
{
if (pSoldier->bLevel == bDestLevel)
{
if ( (pSoldier->bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) )
{
return( FALSE );
}
else // different buildings!
{
return( TRUE );
}
}
else
{
return( TRUE );
}
}
INT16 GetInterveningClimbingLocation( SOLDIERTYPE * pSoldier, INT16 sDestGridNo, INT8 bDestLevel, BOOLEAN * pfClimbingNecessary )
{
if (pSoldier->bLevel == bDestLevel)
{
if ( (pSoldier->bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) )
{
// on ground or same building... normal!
*pfClimbingNecessary = FALSE;
return( NOWHERE );
}
else
{
// different buildings!
// yes, pass in same gridno twice... want closest climb-down spot for building we are on!
*pfClimbingNecessary = TRUE;
return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, pSoldier->sGridNo, FALSE ) );
}
}
else
{
*pfClimbingNecessary = TRUE;
// different levels
if (pSoldier->bLevel == 0)
{
// got to go UP onto building
return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, sDestGridNo, TRUE ) );
}
else
{
// got to go DOWN off building
return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, pSoldier->sGridNo, FALSE ) );
}
}
}
INT16 EstimatePathCostToLocation( SOLDIERTYPE * pSoldier, INT16 sDestGridNo, INT8 bDestLevel, BOOLEAN fAddCostAfterClimbingUp, BOOLEAN * pfClimbingNecessary, INT16 * psClimbGridNo )
{
INT16 sPathCost;
INT16 sClimbGridNo;
if (pSoldier->bLevel == bDestLevel)
{
if ( (pSoldier->bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) )
{
// on ground or same building... normal!
sPathCost = EstimatePlotPath( pSoldier, sDestGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0);
*pfClimbingNecessary = FALSE;
*psClimbGridNo = NOWHERE;
}
else
{
// different buildings!
// yes, pass in same gridno twice... want closest climb-down spot for building we are on!
sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, sDestGridNo, pSoldier->sGridNo, FALSE );
if (sClimbGridNo == NOWHERE)
{
sPathCost = 0;
}
else
{
sPathCost = PlotPath( pSoldier, sClimbGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0 );
if (sPathCost != 0)
{
// add in cost of climbing down
if (fAddCostAfterClimbingUp)
{
// add in cost of later climbing up, too
sPathCost += AP_CLIMBOFFROOF + AP_CLIMBROOF;
// add in an estimate of getting there after climbing down
sPathCost += (AP_MOVEMENT_FLAT + WALKCOST) * PythSpacesAway( sClimbGridNo, sDestGridNo );
}
else
{
sPathCost += AP_CLIMBOFFROOF;
// add in an estimate of getting there after climbing down, *but not on top of roof*
sPathCost += (AP_MOVEMENT_FLAT + WALKCOST) * PythSpacesAway( sClimbGridNo, sDestGridNo ) / 2;
}
*pfClimbingNecessary = TRUE;
*psClimbGridNo = sClimbGridNo;
}
}
}
}
else
{
// different levels
if (pSoldier->bLevel == 0)
{
//got to go UP onto building
sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, sDestGridNo, TRUE );
}
else
{
// got to go DOWN off building
sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, sDestGridNo, pSoldier->sGridNo, FALSE );
}
if (sClimbGridNo == NOWHERE)
{
sPathCost = 0;
}
else
{
sPathCost = PlotPath( pSoldier, sClimbGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0);
if (sPathCost != 0)
{
// add in the cost of climbing up or down
if (pSoldier->bLevel == 0)
{
// must climb up
sPathCost += AP_CLIMBROOF;
if (fAddCostAfterClimbingUp)
{
// add to path a rough estimate of how far to go from the climb gridno to the friend
// estimate walk cost
sPathCost += (AP_MOVEMENT_FLAT + WALKCOST) * PythSpacesAway( sClimbGridNo, sDestGridNo );
}
}
else
{
// must climb down
sPathCost += AP_CLIMBOFFROOF;
// add to path a rough estimate of how far to go from the climb gridno to the friend
// estimate walk cost
sPathCost += (AP_MOVEMENT_FLAT + WALKCOST) * PythSpacesAway( sClimbGridNo, sDestGridNo );
}
*pfClimbingNecessary = TRUE;
*psClimbGridNo = sClimbGridNo;
}
}
}
return( sPathCost );
}
BOOLEAN GuySawEnemyThisTurnOrBefore( SOLDIERTYPE * pSoldier )
{
UINT8 ubTeamLoop;
UINT8 ubIDLoop;
for ( ubTeamLoop = 0; ubTeamLoop < MAXTEAMS; ubTeamLoop++ )
{
if ( gTacticalStatus.Team[ ubTeamLoop ].bSide != pSoldier->bSide )
{
// consider guys in this team, which isn't on our side
for ( ubIDLoop = gTacticalStatus.Team[ ubTeamLoop ].bFirstID; ubIDLoop <= gTacticalStatus.Team[ ubTeamLoop ].bLastID; ubIDLoop++ )
{
// if this guy SAW an enemy recently...
if ( pSoldier->bOppList[ ubIDLoop ] >= SEEN_CURRENTLY )
{
return( TRUE );
}
}
}
}
return( FALSE );
}
INT16 ClosestReachableFriendInTrouble(SOLDIERTYPE *pSoldier, BOOLEAN * pfClimbingNecessary)
{
UINT32 uiLoop;
INT16 sPathCost, sClosestFriend = NOWHERE, sShortestPath = 1000, sClimbGridNo;
BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE;
SOLDIERTYPE *pFriend;
// civilians don't really have any "friends", so they don't bother with this
if (PTR_CIVILIAN)
{
return(sClosestFriend);
}
// consider every friend of this soldier (locations assumed to be known)
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
{
pFriend = MercSlots[ uiLoop ];
// if this merc is inactive, at base, on assignment, or dead
if (!pFriend)
{
continue; // next merc
}
// if this merc is neutral or NOT on the same side, he's not a friend
if (pFriend->bNeutral || (pSoldier->bSide != pFriend->bSide))
{
continue; // next merc
}
// if this "friend" is actually US
if (pFriend->ubID == pSoldier->ubID)
{
continue; // next merc
}
// CJC: restrict "last one to radio" to only if that guy saw us this turn or last turn
// if this friend is not under fire, and isn't the last one to radio
if ( ! ( pFriend->bUnderFire || (pFriend->ubID == gTacticalStatus.Team[pFriend->bTeam].ubLastMercToRadio && GuySawEnemyThisTurnOrBefore( pFriend ) ) ) )
{
continue; // next merc
}
// if we're already neighbors
if (SpacesAway(pSoldier->sGridNo,pFriend->sGridNo) == 1)
{
continue; // next merc
}
// get the AP cost to go to this friend's gridno
sPathCost = EstimatePathCostToLocation( pSoldier, pFriend->sGridNo, pFriend->bLevel, TRUE, &fClimbingNecessary, &sClimbGridNo );
// if we can get there
if (sPathCost != 0)
{
//sprintf((CHAR *)tempstr,"Path cost to friend %s's location is %d",pFriend->name,pathCost);
//PopMessage(tempstr);
if (sPathCost < sShortestPath)
{
if (fClimbingNecessary)
{
sClosestFriend = sClimbGridNo;
}
else
{
sClosestFriend = pFriend->sGridNo;
}
sShortestPath = sPathCost;
fClosestClimbingNecessary = fClimbingNecessary;
}
}
}
#ifdef DEBUGDECISIONS
if (sClosestFriend != NOWHERE)
{
AINumMessage("CLOSEST FRIEND is at gridno ",sClosestFriend);
}
#endif
*pfClimbingNecessary = fClosestClimbingNecessary;
return(sClosestFriend);
}
INT16 DistanceToClosestFriend( SOLDIERTYPE * pSoldier )
{
// find the distance to the closest person on the same team
UINT8 ubLoop;
SOLDIERTYPE *pTargetSoldier;
INT16 sMinDist = 1000;
INT16 sDist;
// Loop through all mercs on player team
ubLoop = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID;
for ( ; ubLoop <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID; ubLoop++ )
{
if (ubLoop == pSoldier->ubID)
{
// same guy - continue!
continue;
}
pTargetSoldier = Menptr + ubLoop;
if ( pSoldier->bActive && pSoldier->bInSector )
{
if (!pTargetSoldier->bActive || !pTargetSoldier->bInSector)
{
continue;
}
// if not conscious, skip him
else if (pTargetSoldier->bLife < OKLIFE)
{
continue;
}
}
else
{
// compare sector #s
if ( (pSoldier->sSectorX != pTargetSoldier->sSectorX) ||
(pSoldier->sSectorY != pTargetSoldier->sSectorY) ||
(pSoldier->bSectorZ != pTargetSoldier->bSectorZ) )
{
continue;
}
else if (pTargetSoldier->bLife < OKLIFE)
{
continue;
}
else
{
// well there's someone who could be near
return( 1 );
}
}
sDist = SpacesAway(pSoldier->sGridNo,pTargetSoldier->sGridNo);
if (sDist < sMinDist)
{
sMinDist = sDist;
}
}
return( sMinDist );
}
BOOLEAN InWaterGasOrSmoke( SOLDIERTYPE *pSoldier, INT16 sGridNo )
{
if (WaterTooDeepForAttacks( sGridNo ))
{
return(TRUE);
}
// smoke
if (gpWorldLevelData[sGridNo].ubExtFlags[ pSoldier->bLevel ] & MAPELEMENT_EXT_SMOKE)
{
return( TRUE );
}
// tear/mustard gas
//if ( (gpWorldLevelData[sGridNo].ubExtFlags[ pSoldier->bLevel ] & (MAPELEMENT_EXT_TEARGAS | MAPELEMENT_EXT_MUSTARDGAS)) &&
// (pSoldier->inv[HEAD1POS].usItem != GASMASK && pSoldier->inv[HEAD2POS].usItem != GASMASK) )
if ( (gpWorldLevelData[sGridNo].ubExtFlags[ pSoldier->bLevel ] & (MAPELEMENT_EXT_TEARGAS | MAPELEMENT_EXT_MUSTARDGAS)) &&
FindGasMask(pSoldier) == NO_SLOT )
{
return( TRUE );
}
if ( gpWorldLevelData[sGridNo].ubExtFlags[ pSoldier->bLevel ] & MAPELEMENT_EXT_BURNABLEGAS )
{
return( TRUE );
}
return(FALSE);
}
BOOLEAN InGasOrSmoke( SOLDIERTYPE *pSoldier, INT16 sGridNo )
{
// smoke
if (gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->bLevel] & MAPELEMENT_EXT_SMOKE)
{
return( TRUE );
}
// tear/mustard gas
//if ( (gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->bLevel] & (MAPELEMENT_EXT_TEARGAS | MAPELEMENT_EXT_MUSTARDGAS)) &&
// (pSoldier->inv[HEAD1POS].usItem != GASMASK && pSoldier->inv[HEAD2POS].usItem != GASMASK) )
if ( (gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->bLevel] & (MAPELEMENT_EXT_TEARGAS | MAPELEMENT_EXT_MUSTARDGAS)) &&
FindGasMask(pSoldier) == NO_SLOT )
{
return( TRUE );
}
if ( gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->bLevel] & MAPELEMENT_EXT_BURNABLEGAS )
{
return( TRUE );
}
return(FALSE);
}
INT16 InWaterOrGas(SOLDIERTYPE *pSoldier, INT16 sGridNo)
{
if (WaterTooDeepForAttacks( sGridNo ))
{
return(TRUE);
}
// tear/mustard gas
if ( (gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->bLevel] & (MAPELEMENT_EXT_TEARGAS | MAPELEMENT_EXT_MUSTARDGAS)) &&
FindGasMask(pSoldier) == NO_SLOT )
{
return( TRUE );
}
if ( gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->bLevel] & MAPELEMENT_EXT_BURNABLEGAS )
{
return( TRUE );
}
return(FALSE);
}
BOOLEAN InGas( SOLDIERTYPE *pSoldier, INT16 sGridNo )
{
// tear/mustard gas
if ( (gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->bLevel] & (MAPELEMENT_EXT_TEARGAS | MAPELEMENT_EXT_MUSTARDGAS)) &&
FindGasMask(pSoldier) == NO_SLOT )
{
return( TRUE );
}
if ( gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->bLevel] & MAPELEMENT_EXT_BURNABLEGAS )
{
return( TRUE );
}
return(FALSE);
}
BOOLEAN WearGasMaskIfAvailable( SOLDIERTYPE * pSoldier )
{
INT8 bSlot, bNewSlot;
bSlot = FindObj( pSoldier, GASMASK );
if ( bSlot == NO_SLOT )
{
return( FALSE );
}
if ( bSlot == HEAD1POS || bSlot == HEAD2POS )
{
return( FALSE );
}
if ( pSoldier->inv[ HEAD1POS ].usItem == NOTHING)
{
bNewSlot = HEAD1POS;
}
else if ( pSoldier->inv[ HEAD2POS ].usItem == NOTHING )
{
bNewSlot = HEAD2POS;
}
else
{
// screw it, going in position 1 anyhow
bNewSlot = HEAD1POS;
}
RearrangePocket( pSoldier, bSlot, bNewSlot, TRUE );
return( TRUE );
}
BOOLEAN InLightAtNight( INT16 sGridNo, INT8 bLevel )
{
UINT8 ubBackgroundLightLevel;
// do not consider us to be "in light" if we're in an underground sector
if ( gbWorldSectorZ > 0 )
{
return( FALSE );
}
ubBackgroundLightLevel = GetTimeOfDayAmbientLightLevel();
if ( ubBackgroundLightLevel < NORMAL_LIGHTLEVEL_DAY + 2 )
{
// don't consider it nighttime, too close to daylight levels
return( FALSE );
}
// could've been placed here, ignore the light
if ( InARoom( sGridNo, NULL ) )
{
return( FALSE );
}
// NB light levels are backwards, so a lower light level means the
// spot in question is BRIGHTER
if ( LightTrueLevel( sGridNo, bLevel ) < ubBackgroundLightLevel )
{
return( TRUE );
}
return( FALSE );
}
INT8 CalcMorale(SOLDIERTYPE *pSoldier)
{
UINT32 uiLoop, uiLoop2;
INT32 iOurTotalThreat = 0, iTheirTotalThreat = 0;
INT16 sOppThreatValue, sFrndThreatValue, sMorale;
INT32 iPercent;
INT8 bMostRecentOpplistValue;
INT8 bMoraleCategory;
UINT8 *pSeenOpp; //,*friendOlPtr;
INT8 *pbPersOL, *pbPublOL;
SOLDIERTYPE *pOpponent,*pFriend;
// if army guy has NO weapons left then panic!
if ( pSoldier->bTeam == ENEMY_TEAM )
{
if ( FindAIUsableObjClass( pSoldier, IC_WEAPON ) == NO_SLOT )
{
return( MORALE_HOPELESS );
}
}
// hang pointers to my personal opplist, my team's public opplist, and my
// list of previously seen opponents
pSeenOpp = (UINT8 *) &(gbSeenOpponents[pSoldier->ubID][0]);
// loop through every one of my possible opponents
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
{
pOpponent = MercSlots[ uiLoop ];
// if this merc is inactive, at base, on assignment, dead, unconscious
if (!pOpponent || (pOpponent->bLife < OKLIFE))
continue; // next merc
// if this merc is neutral/on same side, he's not an opponent, skip him!
if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide))
continue; // next merc
// Special stuff for Carmen the bounty hunter
if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != 64)
{
continue; // next opponent
}
pbPersOL = pSoldier->bOppList + pOpponent->ubID;
pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID;
pSeenOpp = (UINT8 *)gbSeenOpponents[pSoldier->ubID] + pOpponent->ubID;
// if this opponent is unknown to me personally AND unknown to my team, too
if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
{
// if I have never seen him before anywhere in this sector, either
if (!(*pSeenOpp))
continue; // next merc
// have seen him in the past, so he remains something of a threat
bMostRecentOpplistValue = 0; // uses the free slot for 0 opplist
}
else // decide which opplist is more current
{
// if personal knowledge is more up to date or at least equal
if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL))
bMostRecentOpplistValue = *pbPersOL; // use personal
else
bMostRecentOpplistValue = *pbPublOL; // use public
}
iPercent = ThreatPercent[bMostRecentOpplistValue - OLDEST_HEARD_VALUE];
sOppThreatValue = (iPercent * CalcManThreatValue(pOpponent,pSoldier->sGridNo,FALSE,pSoldier)) / 100;
//sprintf((CHAR *)tempstr,"Known opponent %s, opplist status %d, percent %d, threat = %d",
// ExtMen[pOpponent->ubID].name,ubMostRecentOpplistValue,ubPercent,sOppThreatValue);
//PopMessage(tempstr);
// ADD this to their running total threatValue (decreases my MORALE)
iTheirTotalThreat += sOppThreatValue;
//NumMessage("Their TOTAL threat now = ",sTheirTotalThreat);
// NOW THE FUN PART: SINCE THIS OPPONENT IS KNOWN TO ME IN SOME WAY,
// ANY FRIENDS OF MINE THAT KNOW ABOUT HIM BOOST MY MORALE. SO, LET'S GO
// THROUGH THEIR PERSONAL OPPLISTS AND CHECK WHICH OF MY FRIENDS KNOW
// SOMETHING ABOUT HIM AND WHAT THEIR THREAT VALUE TO HIM IS.
for (uiLoop2 = 0; uiLoop2 < guiNumMercSlots; uiLoop2++)
{
pFriend = MercSlots[ uiLoop2 ];
// if this merc is inactive, at base, on assignment, dead, unconscious
if (!pFriend || (pFriend->bLife < OKLIFE))
continue; // next merc
// if this merc is not on my side, then he's NOT one of my friends
// WE CAN'T AFFORD TO CONSIDER THE ENEMY OF MY ENEMY MY FRIEND, HERE!
// ONLY IF WE ARE ACTUALLY OFFICIALLY CO-OPERATING TOGETHER (SAME SIDE)
if ( pFriend->bNeutral && !( pSoldier->ubCivilianGroup != NON_CIV_GROUP && pSoldier->ubCivilianGroup == pFriend->ubCivilianGroup ) )
{
continue; // next merc
}
if ( pSoldier->bSide != pFriend->bSide )
continue; // next merc
// THIS TEST IS INVALID IF A COMPUTER-TEAM IS PLAYING CO-OPERATIVELY
// WITH A NON-COMPUTER TEAM SINCE THE OPPLISTS INVOLVED ARE NOT
// UP-TO-DATE. THIS SITUATION IS CURRENTLY NOT POSSIBLE IN HTH/DG.
// ALSO NOTE THAT WE COUNT US AS OUR (BEST) FRIEND FOR THESE CALCULATIONS
// subtract HEARD_2_TURNS_AGO (which is negative) to make values start at 0 and
// be positive otherwise
iPercent = ThreatPercent[pFriend->bOppList[pOpponent->ubID] - OLDEST_HEARD_VALUE];
// reduce the percentage value based on how far away they are from the enemy, if they only hear him
if ( pFriend->bOppList[ pOpponent->ubID ] <= HEARD_LAST_TURN )
{
iPercent -= PythSpacesAway( pSoldier->sGridNo, pFriend->sGridNo ) * 2;
if ( iPercent <= 0 )
{
//ignore!
continue;
}
}
sFrndThreatValue = (iPercent * CalcManThreatValue(pFriend,pOpponent->sGridNo,FALSE,pSoldier)) / 100;
//sprintf((CHAR *)tempstr,"Known by friend %s, opplist status %d, percent %d, threat = %d",
// ExtMen[pFriend->ubID].name,pFriend->bOppList[pOpponent->ubID],ubPercent,sFrndThreatValue);
//PopMessage(tempstr);
// ADD this to our running total threatValue (increases my MORALE)
// We multiply by sOppThreatValue to PRO-RATE this based on opponent's
// threat value to ME personally. Divide later by sum of them all.
iOurTotalThreat += sOppThreatValue * sFrndThreatValue;
}
// this could get slow if I have a lot of friends...
//KeepInterfaceGoing();
}
// if they are no threat whatsoever
if (!iTheirTotalThreat)
sMorale = 500; // our morale is just incredible
else
{
// now divide sOutTotalThreat by sTheirTotalThreat to get the REAL value
iOurTotalThreat /= iTheirTotalThreat;
// calculate the morale (100 is even, < 100 is us losing, > 100 is good)
sMorale = (INT16) ((100 * iOurTotalThreat) / iTheirTotalThreat);
}
if (sMorale <= 25) // odds 1:4 or worse
bMoraleCategory = MORALE_HOPELESS;
else if (sMorale <= 50) // odds between 1:4 and 1:2
bMoraleCategory = MORALE_WORRIED;
else if (sMorale <= 150) // odds between 1:2 and 3:2
bMoraleCategory = MORALE_NORMAL;
else if (sMorale <= 300) // odds between 3:2 and 3:1
bMoraleCategory = MORALE_CONFIDENT;
else // odds better than 3:1
bMoraleCategory = MORALE_FEARLESS;
switch (pSoldier->bAttitude)
{
case DEFENSIVE: bMoraleCategory--; break;
case BRAVESOLO: bMoraleCategory += 2; break;
case BRAVEAID: bMoraleCategory += 2; break;
case CUNNINGSOLO: break;
case CUNNINGAID: break;
case AGGRESSIVE: bMoraleCategory++; break;
}
// make idiot administrators much more aggressive
if ( pSoldier->ubSoldierClass == SOLDIER_CLASS_ADMINISTRATOR )
{
bMoraleCategory += 2;
}
// if still full of energy
if (pSoldier->bBreath > 75)
bMoraleCategory++;
else
{
// if getting a bit low on breath
if (pSoldier->bBreath < 40)
bMoraleCategory--;
// if getting REALLY low on breath
if (pSoldier->bBreath < 10)
bMoraleCategory--;
}
// if still very healthy
if (pSoldier->bLife > 75)
bMoraleCategory++;
else
{
// if getting a bit low on life
if (pSoldier->bLife < 40)
bMoraleCategory--;
// if getting REALLY low on life
if (pSoldier->bLife < 20)
bMoraleCategory--;
}
// if soldier is currently not under fire
if (!pSoldier->bUnderFire)
bMoraleCategory++;
// if adjustments made it outside the allowed limits
if (bMoraleCategory < MORALE_HOPELESS)
bMoraleCategory = MORALE_HOPELESS;
else
{
if (bMoraleCategory > MORALE_FEARLESS)
bMoraleCategory = MORALE_FEARLESS;
}
// if only 1/4 of side left, reduce morale
// and do this after we've capped all those other silly values
/*
if ( pSoldier->bTeam == ENEMY_TEAM && gTacticalStatus.Team[ ENEMY_TEAM ].bMenInSector <= gTacticalStatus.bOriginalSizeOfEnemyForce / 4 )
{
bMoraleCategory -= 2;
if (bMoraleCategory < MORALE_HOPELESS)
bMoraleCategory = MORALE_HOPELESS;
}
*/
// brave guys never get hopeless, at worst they get worried
if (bMoraleCategory == MORALE_HOPELESS &&
(pSoldier->bAttitude == BRAVESOLO || pSoldier->bAttitude == BRAVEAID))
bMoraleCategory = MORALE_WORRIED;
#ifdef DEBUGDECISIONS
STR tempstr;
sprintf((CHAR *) tempstr, "Morale = %d (category %d)\n",
pSoldier->bMorale,bMoraleCategory);
DebugAI (tempstr);
#endif
return(bMoraleCategory);
}
INT32 CalcManThreatValue( SOLDIERTYPE *pEnemy, INT16 sMyGrid, UINT8 ubReduceForCover, SOLDIERTYPE * pMe )
{
INT32 iThreatValue = 0;
BOOLEAN fForCreature = CREATURE_OR_BLOODCAT( pMe );
// If man is inactive, at base, on assignment, dead, unconscious
if (!pEnemy->bActive || !pEnemy->bInSector || !pEnemy->bLife)
{
// he's no threat at all, return a negative number
iThreatValue = -999;
return(iThreatValue);
}
// in boxing mode, let only a boxer be considered a threat.
if ( (gTacticalStatus.bBoxingState == BOXING) && !(pEnemy->uiStatusFlags & SOLDIER_BOXER) )
{
iThreatValue = -999;
return( iThreatValue );
}
if (fForCreature)
{
// health (1-100)
iThreatValue += pEnemy->bLife;
// bleeding (more attactive!) (1-100)
iThreatValue += pEnemy->bBleeding;
// decrease according to distance
iThreatValue = (iThreatValue * 10) / (10 + PythSpacesAway( sMyGrid, pEnemy->sGridNo ) );
}
else
{
// ADD twice the man's level (2-20)
iThreatValue += pEnemy->bExpLevel;
// ADD man's total action points (10-35)
iThreatValue += CalcActionPoints(pEnemy);
// ADD 1/2 of man's current action points (4-17)
iThreatValue += (pEnemy->bActionPoints / 2);
// ADD 1/10 of man's current health (0-10)
iThreatValue += (pEnemy->bLife / 10);
if (pEnemy->bAssignment < ON_DUTY )
{
// ADD 1/4 of man's protection percentage (0-25)
iThreatValue += ArmourPercent( pEnemy ) / 4;
// ADD 1/5 of man's marksmanship skill (0-20)
iThreatValue += (pEnemy->bMarksmanship / 5);
if ( Item[ pEnemy->inv[HANDPOS].usItem ].usItemClass & IC_WEAPON )
{
// ADD the deadliness of the item(weapon) he's holding (0-50)
iThreatValue += Weapon[pEnemy->inv[HANDPOS].usItem].ubDeadliness;
}
}
// SUBTRACT 1/5 of man's bleeding (0-20)
iThreatValue -= (pEnemy->bBleeding / 5);
// SUBTRACT 1/10 of man's breath deficiency (0-10)
iThreatValue -= ((100 - pEnemy->bBreath) / 10);
// SUBTRACT man's current shock value
iThreatValue -= pEnemy->bShock;
}
// if I have a specifically defined spot where I'm at (sometime I don't!)
if (sMyGrid != NOWHERE)
{
// ADD 10% if man's already been shooting at me
if (pEnemy->sLastTarget == sMyGrid)
{
iThreatValue += (iThreatValue / 10);
}
else
{
// ADD 5% if man's already facing me
if (pEnemy->bDirection == atan8(CenterX(pEnemy->sGridNo),CenterY(pEnemy->sGridNo),CenterX(sMyGrid),CenterY(sMyGrid)))
{
iThreatValue += (iThreatValue / 20);
}
}
}
// if this man is conscious
if (pEnemy->bLife >= OKLIFE)
{
// and we were told to reduce threat for my cover
if (ubReduceForCover && (sMyGrid != NOWHERE))
{
// Reduce iThreatValue to same % as the chance HE has shoot through at ME
//iThreatValue = (iThreatValue * ChanceToGetThrough( pEnemy, myGrid, FAKE, ACTUAL, TESTWALLS, 9999, M9PISTOL, NOT_FOR_LOS)) / 100;
//iThreatValue = (iThreatValue * SoldierTo3DLocationChanceToGetThrough( pEnemy, myGrid, FAKE, ACTUAL, TESTWALLS, 9999, M9PISTOL, NOT_FOR_LOS)) / 100;
iThreatValue = (iThreatValue * SoldierToLocationChanceToGetThrough( pEnemy, sMyGrid, pMe->bLevel, 0, pMe->ubID ) ) / 100;
}
}
else
{
// if he's still something of a threat
if (iThreatValue > 0)
{
// drastically reduce his threat value (divide by 5 to 18)
iThreatValue /= (4 + (OKLIFE - pEnemy->bLife));
}
}
// threat value of any opponent can never drop below 1
if (iThreatValue < 1)
{
iThreatValue = 1;
}
//sprintf((CHAR *)tempstr,"%s's iThreatValue = ",pEnemy->name);
//NumMessage(tempstr,iThreatValue);
#ifdef BETAVERSION // unnecessary for real release
// NOTE: maximum is about 200 for a healthy Mike type with a mortar!
if (iThreatValue > 250)
{
sprintf((CHAR *)tempstr,"CalcManThreatValue: WARNING - %d has a very high threat value of %d",pEnemy->ubID,iThreatValue);
#ifdef RECORDNET
fprintf(NetDebugFile,"\t%s\n",tempstr);
#endif
#ifdef TESTVERSION
PopMessage(tempstr);
#endif
}
#endif
return(iThreatValue);
}
INT16 RoamingRange(SOLDIERTYPE *pSoldier, INT16 * pusFromGridNo)
{
if( pSoldier->bBleeding > 0 )return MAX_ROAMING_RANGE;
if ( CREATURE_OR_BLOODCAT( pSoldier ) )
{
if ( pSoldier->bAlertStatus == STATUS_BLACK )
{
*pusFromGridNo = pSoldier->sGridNo; // from current position!
return(MAX_ROAMING_RANGE);
}
}
if ( pSoldier->bOrders == POINTPATROL || pSoldier->bOrders == RNDPTPATROL )
{
// roam near NEXT PATROL POINT, not from where merc starts out
*pusFromGridNo = pSoldier->usPatrolGrid[pSoldier->bNextPatrolPnt];
}
else
{
// roam around where mercs started
//*pusFromGridNo = pSoldier->sInitialGridNo;
*pusFromGridNo = pSoldier->usPatrolGrid[0];
}
switch (pSoldier->bOrders)
{
// JA2 GOLD: give non-NPCs a 5 tile roam range for cover in combat when being shot at
case STATIONARY: if (pSoldier->ubProfile != NO_PROFILE || (pSoldier->bAlertStatus < STATUS_BLACK && !(pSoldier->bUnderFire)))
{
return( 0 );
}
else
{
return( 5 );
} case ONGUARD: return( 5 );
case CLOSEPATROL: return( 15 );
case RNDPTPATROL:
case POINTPATROL: return(10 ); // from nextPatrolGrid, not whereIWas
case FARPATROL: if (pSoldier->bAlertStatus < STATUS_RED)
{
return( 25 );
}
else
{
return( 50 );
}
case ONCALL: if (pSoldier->bAlertStatus < STATUS_RED)
{
return( 10 );
}
else
{
return( 30 );
}
case SEEKENEMY: *pusFromGridNo = pSoldier->sGridNo; // from current position!
return(MAX_ROAMING_RANGE);
default:
#ifdef BETAVERSION
sprintf((CHAR *)tempstr,"%s has invalid orders = %d",pSoldier->name,pSoldier->bOrders);
PopMessage(tempstr);
#endif
return(0);
}
}
void RearrangePocket(SOLDIERTYPE *pSoldier, INT8 bPocket1, INT8 bPocket2, UINT8 bPermanent)
{
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"RearrangePocket");
// NB there's no such thing as a temporary swap for now...
SwapObjs( &(pSoldier->inv[bPocket1]), &(pSoldier->inv[bPocket2]) );
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"RearrangePocket done");
}
BOOLEAN FindBetterSpotForItem( SOLDIERTYPE * pSoldier, INT8 bSlot )
{
// looks for a place in the slots to put an item in a hand or armour
// position, and moves it there.
if (bSlot >= BIGPOCK1POS)
{
return( FALSE );
}
if (pSoldier->inv[bSlot].usItem == NOTHING)
{
// well that's just fine then!
return( TRUE );
}
if (Item[pSoldier->inv[bSlot].usItem].ubPerPocket == 0)
{
// then we're looking for a big pocket
bSlot = FindEmptySlotWithin( pSoldier, BIGPOCK1POS, BIGPOCK4POS );
}
else
{
// try a small pocket first
bSlot = FindEmptySlotWithin( pSoldier, SMALLPOCK1POS, SMALLPOCK8POS );
if (bSlot == NO_SLOT)
{
bSlot = FindEmptySlotWithin( pSoldier, BIGPOCK1POS, BIGPOCK4POS );
}
}
if (bSlot == NO_SLOT)
{
return( FALSE );
}
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"findbetterspotforitem: swapping items");
RearrangePocket(pSoldier, HANDPOS, bSlot, FOREVER );
return( TRUE );
}
UINT8 GetTraversalQuoteActionID( INT8 bDirection )
{
switch( bDirection )
{
case NORTHEAST: // east
return( QUOTE_ACTION_ID_TRAVERSE_EAST );
case SOUTHEAST: // south
return( QUOTE_ACTION_ID_TRAVERSE_SOUTH );
case SOUTHWEST: // west
return( QUOTE_ACTION_ID_TRAVERSE_WEST );
case NORTHWEST: // north
return( QUOTE_ACTION_ID_TRAVERSE_NORTH );
default:
return( 0 );
}
}
UINT8 SoldierDifficultyLevel( SOLDIERTYPE * pSoldier )
{
INT8 bDifficultyBase;
INT8 bDifficulty;
DebugMsg(TOPIC_JA2AI,DBG_LEVEL_3,String("SoldierDifficultyLevel"));
// difficulty modifier ranges from 0 to 100
// and we want to end up with a number between 0 and 4 (4=hardest)
// to a base of 1, divide by 34 to get a range from 1 to 3
bDifficultyBase = 1 + ( CalcDifficultyModifier( pSoldier->ubSoldierClass ) / 34 );
switch( pSoldier->ubSoldierClass )
{
case SOLDIER_CLASS_ADMINISTRATOR:
bDifficulty = bDifficultyBase - 1;
break;
case SOLDIER_CLASS_ARMY:
bDifficulty = bDifficultyBase;
break;
case SOLDIER_CLASS_ELITE:
bDifficulty = bDifficultyBase + 1;
break;
// hard code militia;
case SOLDIER_CLASS_GREEN_MILITIA:
bDifficulty = 2;
break;
case SOLDIER_CLASS_REG_MILITIA:
bDifficulty = 3;
break;
case SOLDIER_CLASS_ELITE_MILITIA:
bDifficulty = 4;
break;
default:
if (pSoldier->bTeam == CREATURE_TEAM)
{
bDifficulty = bDifficultyBase + pSoldier->bLevel / 4;
}
else // civ...
{
bDifficulty = (bDifficultyBase + pSoldier->bLevel / 4) - 1;
}
break;
}
bDifficulty = __max( bDifficulty, 0 );
bDifficulty = __min( bDifficulty, 4 );
return( (UINT8) bDifficulty );
}
BOOLEAN ValidCreatureTurn( SOLDIERTYPE * pCreature, INT8 bNewDirection )
{
INT8 bDirChange;
INT8 bTempDir;
INT8 bLoop;
BOOLEAN fFound;
bDirChange = (INT8) QuickestDirection( pCreature->bDirection, bNewDirection );
for( bLoop = 0; bLoop < 2; bLoop++ )
{
fFound = TRUE;
bTempDir = pCreature->bDirection;
do
{
bTempDir += bDirChange;
if (bTempDir < NORTH)
{
bTempDir = NORTHWEST;
}
else if (bTempDir > NORTHWEST)
{
bTempDir = NORTH;
}
if (!InternalIsValidStance( pCreature, bTempDir, ANIM_STAND ))
{
fFound = FALSE;
break;
}
} while ( bTempDir != bNewDirection );
if ( fFound )
{
break;
}
else if ( bLoop > 0 )
{
// can't find a dir!
return( FALSE );
}
else
{
// try the other direction
bDirChange = bDirChange * -1;
}
}
return( TRUE );
}
INT32 RangeChangeDesire( SOLDIERTYPE * pSoldier )
{
INT32 iRangeFactorMultiplier;
iRangeFactorMultiplier = pSoldier->bAIMorale - 1;
switch (pSoldier->bAttitude)
{
case DEFENSIVE: iRangeFactorMultiplier += -1; break;
case BRAVESOLO: iRangeFactorMultiplier += 2; break;
case BRAVEAID: iRangeFactorMultiplier += 2; break;
case CUNNINGSOLO: iRangeFactorMultiplier += 0; break;
case CUNNINGAID: iRangeFactorMultiplier += 0; break;
case ATTACKSLAYONLY:
case AGGRESSIVE: iRangeFactorMultiplier += 1; break;
}
if ( gTacticalStatus.bConsNumTurnsWeHaventSeenButEnemyDoes > 0 )
{
iRangeFactorMultiplier += gTacticalStatus.bConsNumTurnsWeHaventSeenButEnemyDoes;
}
return( iRangeFactorMultiplier );
}
BOOLEAN ArmySeesOpponents( void )
{
INT32 cnt;
SOLDIERTYPE * pSoldier;
for ( cnt = gTacticalStatus.Team[ ENEMY_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ ENEMY_TEAM ].bLastID; cnt++ )
{
pSoldier = MercPtrs[ cnt ];
if ( pSoldier->bActive && pSoldier->bInSector && pSoldier->bLife >= OKLIFE && pSoldier->bOppCnt > 0 )
{
return( TRUE );
}
}
return( FALSE );
}