mirror of
https://github.com/1dot13/source.git
synced 2026-07-22 13:40:22 +02:00
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.
6252 lines
171 KiB
C++
6252 lines
171 KiB
C++
#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 "message.h"
|
|
#include "LOS.h"
|
|
#include "Assignments.h"
|
|
#include "Soldier Functions.h"
|
|
#include "Points.h"
|
|
#include "GameSettings.h"
|
|
#include "Buildings.h"
|
|
#include "Soldier macros.h"
|
|
#include "Render Fun.h"
|
|
#include "strategicmap.h"
|
|
#include "environment.h"
|
|
#include "lighting.h"
|
|
#include "Soldier Create.h"
|
|
#include "SkillCheck.h" // added by SANDRO
|
|
#include "Vehicles.h" // added by silversurfer
|
|
#include "Game Clock.h" // sevenfm
|
|
#include "Rotting Corpses.h" // sevenfm
|
|
#include "WCheck.h" // sevenfm
|
|
#include "SmokeEffects.h" // sevenfm
|
|
|
|
#include "GameInitOptionsScreen.h"
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// SANDRO - In this file, all APBPConstants[AP_CROUCH] and APBPConstants[AP_PRONE] were changed to GetAPsCrouch() and GetAPsProne()
|
|
// On the bottom here, there are these functions made
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
//
|
|
// CJC's DG->JA2 conversion notes
|
|
//
|
|
// Commented out:
|
|
//
|
|
// InWaterOrGas - gas stuff
|
|
// RoamingRange - point patrol stuff
|
|
|
|
// sevenfm
|
|
extern UINT16 PickSoldierReadyAnimation(SOLDIERTYPE *pSoldier, BOOLEAN fEndReady, BOOLEAN fHipStance);
|
|
extern SECTOR_EXT_DATA SectorExternalData[256][4];
|
|
|
|
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
|
|
{RUNNING, RUNNING, SWATTING}, // flank left
|
|
{RUNNING, RUNNING, 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, pSoldier->pathing.bLevel))
|
|
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 (ItemIsCannon(pSoldier->inv[HANDPOS].usItem))
|
|
{
|
|
// 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][0]->data.gun.ubGunShotsLeft == 0 /*SB*/ ||
|
|
!(pSoldier->inv[HANDPOS][0]->data.gun.ubGunState & GS_CARTRIDGE_IN_CHAMBER) ||
|
|
(pSoldier->IsValidSecondHandShotForReloadingPurposes( ) &&
|
|
(pSoldier->inv[SECONDHANDPOS][0]->data.gun.ubGunShotsLeft == 0 ||
|
|
!(pSoldier->inv[SECONDHANDPOS][0]->data.gun.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 )
|
|
{
|
|
INT32 sOpponentGridNo;
|
|
INT8 bOpponentLevel;
|
|
INT32 iRangeInCellCoords;
|
|
|
|
if (pSoldier->aiData.bAIMorale >= MORALE_NORMAL)
|
|
{
|
|
return( FALSE );
|
|
}
|
|
// We don't want to go prone if there is a nearby enemy
|
|
ClosestKnownOpponent( pSoldier, &sOpponentGridNo, &bOpponentLevel, nullptr, &iRangeInCellCoords);
|
|
if ( iRangeInCellCoords > 10*CELL_X_SIZE)
|
|
{
|
|
return( TRUE );
|
|
}
|
|
else
|
|
{
|
|
return( FALSE );
|
|
}
|
|
}
|
|
|
|
UINT8 StanceChange( SOLDIERTYPE * pSoldier, INT16 ubAttackAPCost )
|
|
{
|
|
// consider crouching or going prone
|
|
|
|
if (PTR_STANDING)
|
|
{
|
|
if (pSoldier->bActionPoints - ubAttackAPCost >= GetAPsCrouch(pSoldier, TRUE))
|
|
{
|
|
if ( (pSoldier->bActionPoints - ubAttackAPCost >= GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE)) && 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 >= GetAPsProne(pSoldier, TRUE)) && 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, bCurAimTime, bSetScopeMode;
|
|
UINT32 uiChanceOfDamage, uiBestChanceOfDamage, uiCurrChanceOfDamage;
|
|
UINT32 uiStanceBonus, uiMinimumStanceBonusPerChange = 20 - 3 * pAttack->ubAimTime;
|
|
INT32 iRange;
|
|
|
|
bStanceNum = 0;
|
|
uiCurrChanceOfDamage = 0;
|
|
|
|
bSetScopeMode = pSoldier->bScopeMode;
|
|
pSoldier->bScopeMode = pAttack->bScopeMode;
|
|
bAPsAfterAttack = pSoldier->bActionPoints - MinAPsToAttack( pSoldier, pAttack->sTarget, ADDTURNCOST, pAttack->ubAimTime, 1);
|
|
pSoldier->bScopeMode = bSetScopeMode;
|
|
if (bAPsAfterAttack < GetAPsCrouch(pSoldier, TRUE))
|
|
{
|
|
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 < GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE))
|
|
{
|
|
// can't consider this!
|
|
continue;
|
|
}
|
|
|
|
switch( bLoop )
|
|
{
|
|
case 0:
|
|
if ( !pSoldier->InternalIsValidStance( bDesiredDirection, ANIM_STAND ) )
|
|
{
|
|
continue;
|
|
}
|
|
pSoldier->usAnimState = STANDING;
|
|
break;
|
|
case 1:
|
|
if ( !pSoldier->InternalIsValidStance( bDesiredDirection, ANIM_CROUCH ) )
|
|
{
|
|
continue;
|
|
}
|
|
pSoldier->usAnimState = CROUCHING;
|
|
break;
|
|
default:
|
|
if ( !pSoldier->InternalIsValidStance( bDesiredDirection, ANIM_PRONE ) )
|
|
{
|
|
continue;
|
|
}
|
|
pSoldier->usAnimState = PRONE;
|
|
break;
|
|
}
|
|
|
|
// Hack: Assumes the cost to reach the target stance from the current stance is the same as going back. Probably true.
|
|
bCurAimTime = __min( bAPsAfterAttack - GetAPsToChangeStance( pSoldier, bStanceNum), pAttack->ubAimTime);
|
|
// If can't fire at all from this stance, don't bother with the chances of hitting
|
|
if (bCurAimTime < 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
uiChanceOfDamage = SoldierToLocationChanceToGetThrough( pSoldier, pAttack->sTarget, pSoldier->bTargetLevel, pSoldier->bTargetCubeLevel, pAttack->ubOpponent ) * CalcChanceToHitGun( pSoldier, pAttack->sTarget, bCurAimTime, 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->IsFastMovement() )
|
|
{
|
|
return( RUNNING );
|
|
}
|
|
else if ( CREATURE_OR_BLOODCAT( pSoldier ) )
|
|
{
|
|
if (pSoldier->aiData.bAlertStatus == STATUS_GREEN)
|
|
return( WALKING );
|
|
else
|
|
return( RUNNING );
|
|
}
|
|
// zombies always run if they know enemy location
|
|
else if (gGameExternalOptions.fAIMovementMode &&
|
|
pSoldier->IsZombie() &&
|
|
IS_MERC_BODY_TYPE(pSoldier))
|
|
{
|
|
if (!TileIsOutOfBounds(ClosestKnownOpponent(pSoldier, NULL, NULL)))
|
|
return RUNNING;
|
|
else
|
|
return WALKING;
|
|
}
|
|
else if (pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CROW)
|
|
{
|
|
return( WALKING );
|
|
}
|
|
else
|
|
{
|
|
if ( (pSoldier->aiData.fAIFlags & AI_CAUTIOUS) && (MovementMode[bAction][Urgency[pSoldier->aiData.bAlertStatus][pSoldier->aiData.bAIMorale]] == RUNNING) )
|
|
{
|
|
return( WALKING );
|
|
}
|
|
|
|
// if soldier is already crouched/prone, use SWATTING
|
|
if ((pSoldier->aiData.fAIFlags & AI_CAUTIOUS) &&
|
|
gGameExternalOptions.fAIMovementMode &&
|
|
IS_MERC_BODY_TYPE(pSoldier) &&
|
|
(pSoldier->bTeam == ENEMY_TEAM || pSoldier->bTeam == MILITIA_TEAM) &&
|
|
gAnimControl[pSoldier->usAnimState].ubEndHeight <= ANIM_CROUCH)
|
|
{
|
|
return SWATTING;
|
|
}
|
|
if ((pSoldier->aiData.fAIFlags & AI_CAUTIOUS) && (MovementMode[bAction][Urgency[pSoldier->aiData.bAlertStatus][pSoldier->aiData.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->aiData.bAlertStatus == STATUS_GREEN) && Random( 10 ) == 0 )
|
|
{
|
|
return( KID_SKIPPING );
|
|
}
|
|
else
|
|
{
|
|
// sevenfm: movement mode tweaks
|
|
if (gGameExternalOptions.fAIMovementMode)
|
|
{
|
|
INT32 distanceToThreat;
|
|
const INT32 sClosestThreat = ClosestKnownOpponent(pSoldier, NULL, NULL, NULL, &distanceToThreat);
|
|
const auto mediumRange = TACTICAL_RANGE_CELL_COORDS / 2;
|
|
const auto close = TACTICAL_RANGE_CELL_COORDS / 4;
|
|
const auto reallyClose = TACTICAL_RANGE_CELL_COORDS / 8;
|
|
|
|
// use walking mode if no enemy known
|
|
if (pSoldier->aiData.bAlertStatus < STATUS_RED &&
|
|
TileIsOutOfBounds(sClosestThreat) &&
|
|
!pSoldier->aiData.bUnderFire &&
|
|
(bAction == AI_ACTION_SEEK_FRIEND || bAction == AI_ACTION_SEEK_NOISE || bAction == AI_ACTION_TAKE_COVER))
|
|
{
|
|
return WALKING;
|
|
}
|
|
|
|
// use swatting when blinded
|
|
if (IS_MERC_BODY_TYPE(pSoldier) &&
|
|
pSoldier->bBlindedCounter > 0)
|
|
{
|
|
return SWATTING;
|
|
}
|
|
|
|
if (IS_MERC_BODY_TYPE(pSoldier) &&
|
|
pSoldier->aiData.bAlertStatus >= STATUS_YELLOW &&
|
|
!InWaterGasOrSmoke(pSoldier, pSoldier->sGridNo) &&
|
|
!BOXER(pSoldier) &&
|
|
!TileIsOutOfBounds(sClosestThreat) &&
|
|
(pSoldier->bTeam == ENEMY_TEAM || pSoldier->bTeam == MILITIA_TEAM))
|
|
{
|
|
INT16 sDistanceVisible = VISION_RANGE;
|
|
const auto beyondVisionRange = (CELL_X_SIZE * 3 * sDistanceVisible) / 2;
|
|
INT32 iRCD = RangeChangeDesire(pSoldier);
|
|
|
|
// use running when in light at night
|
|
if (NightTime() &&
|
|
InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) &&
|
|
(bAction == AI_ACTION_SEEK_OPPONENT ||
|
|
bAction == AI_ACTION_GET_CLOSER ||
|
|
bAction == AI_ACTION_SEEK_FRIEND ||
|
|
bAction == AI_ACTION_TAKE_COVER))
|
|
{
|
|
return RUNNING;
|
|
}
|
|
|
|
// use swatting for seeking at night or when soldier is already crouched
|
|
if (!InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) &&
|
|
pSoldier->aiData.bAlertStatus == STATUS_RED &&
|
|
iRCD < 4 &&
|
|
!pSoldier->aiData.bUnderFire &&
|
|
pSoldier->aiData.bShock == 0 &&
|
|
!GuySawEnemy(pSoldier) &&
|
|
(NightTime() || gAnimControl[pSoldier->usAnimState].ubEndHeight <= ANIM_CROUCH) &&
|
|
CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) < 3 &&
|
|
distanceToThreat < beyondVisionRange &&
|
|
CountFriendsBlack(pSoldier) == 0 &&
|
|
bAction == AI_ACTION_SEEK_OPPONENT)
|
|
{
|
|
return SWATTING;
|
|
}
|
|
|
|
// use swatting for taking cover
|
|
if (pSoldier->aiData.bAlertStatus >= STATUS_RED &&
|
|
distanceToThreat > reallyClose &&
|
|
(pSoldier->aiData.bUnderFire && iRCD < 4 ||
|
|
pSoldier->aiData.bShock > 2 * iRCD ||
|
|
pSoldier->aiData.bShock > 0 && gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE) &&
|
|
!pSoldier->aiData.bLastAttackHit &&
|
|
bAction == AI_ACTION_TAKE_COVER)
|
|
{
|
|
return SWATTING;
|
|
}
|
|
|
|
// use SWATTING when under fire
|
|
if (pSoldier->aiData.bAlertStatus >= STATUS_RED &&
|
|
(pSoldier->aiData.bShock > iRCD && distanceToThreat > mediumRange ||
|
|
pSoldier->aiData.bShock > 0 && gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE && distanceToThreat > close) &&
|
|
distanceToThreat < beyondVisionRange &&
|
|
gAnimControl[pSoldier->usAnimState].ubEndHeight <= ANIM_CROUCH &&
|
|
!pSoldier->aiData.bLastAttackHit &&
|
|
(bAction == AI_ACTION_SEEK_OPPONENT ||
|
|
bAction == AI_ACTION_GET_CLOSER ||
|
|
bAction == AI_ACTION_SEEK_FRIEND ||
|
|
bAction == AI_ACTION_TAKE_COVER))
|
|
{
|
|
return SWATTING;
|
|
}
|
|
|
|
// use SWATTING when in a room and seen enemy recently or under fire
|
|
if (InARoom(pSoldier->sGridNo, NULL) &&
|
|
pSoldier->aiData.bAlertStatus >= STATUS_YELLOW &&
|
|
(pSoldier->aiData.bOrders == SNIPER ||
|
|
pSoldier->aiData.bOrders == STATIONARY ||
|
|
(GuySawEnemy(pSoldier) || pSoldier->aiData.bShock > 0) && iRCD < 4) &&
|
|
distanceToThreat > close &&
|
|
(bAction == AI_ACTION_SEEK_OPPONENT ||
|
|
bAction == AI_ACTION_GET_CLOSER ||
|
|
bAction == AI_ACTION_SEEK_FRIEND ||
|
|
bAction == AI_ACTION_TAKE_COVER ||
|
|
bAction == AI_ACTION_SEEK_NOISE))
|
|
{
|
|
return SWATTING;
|
|
}
|
|
|
|
// use swatting for snipers on roof or when under fire
|
|
if (pSoldier->pathing.bLevel > 0 &&
|
|
pSoldier->aiData.bAlertStatus >= STATUS_YELLOW &&
|
|
(pSoldier->aiData.bOrders == SNIPER ||
|
|
pSoldier->aiData.bOrders == STATIONARY ||
|
|
pSoldier->aiData.bShock > 0 && iRCD < 4) &&
|
|
distanceToThreat > close &&
|
|
(bAction == AI_ACTION_SEEK_OPPONENT ||
|
|
bAction == AI_ACTION_GET_CLOSER ||
|
|
bAction == AI_ACTION_SEEK_FRIEND ||
|
|
bAction == AI_ACTION_TAKE_COVER ||
|
|
bAction == AI_ACTION_SEEK_NOISE))
|
|
{
|
|
return SWATTING;
|
|
}
|
|
|
|
// use running for taking cover when not under attack
|
|
if (!pSoldier->aiData.bUnderFire &&
|
|
bAction == AI_ACTION_TAKE_COVER &&
|
|
pSoldier->bInitialActionPoints > APBPConstants[AP_MINIMUM] &&
|
|
(!InARoom(pSoldier->sGridNo, NULL) || distanceToThreat > sDistanceVisible * 20) &&
|
|
pSoldier->aiData.bAIMorale >= MORALE_NORMAL &&
|
|
pSoldier->bBreath > 25 &&
|
|
pSoldier->pathing.bLevel == 0 &&
|
|
pSoldier->aiData.bOrders != STATIONARY &&
|
|
pSoldier->aiData.bOrders != SNIPER &&
|
|
(gAnimControl[pSoldier->usAnimState].ubEndHeight > ANIM_PRONE || pSoldier->bActionPoints > APBPConstants[AP_MINIMUM]))
|
|
{
|
|
return RUNNING;
|
|
}
|
|
|
|
// decide movement mode when getting closer
|
|
if (bAction == AI_ACTION_GET_CLOSER)
|
|
{
|
|
if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_STAND)
|
|
{
|
|
if (WeaponReady(pSoldier) && !pSoldier->aiData.bUnderFire && pSoldier->aiData.bAlertStatus == STATUS_BLACK)
|
|
return WALKING;
|
|
else
|
|
return RUNNING;
|
|
}
|
|
else if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_CROUCH)
|
|
{
|
|
if (WeaponReady(pSoldier) && !pSoldier->aiData.bUnderFire && pSoldier->aiData.bAlertStatus == STATUS_BLACK ||
|
|
pSoldier->aiData.bUnderFire && distanceToThreat > reallyClose )
|
|
return SWATTING;
|
|
else
|
|
return RUNNING;
|
|
}
|
|
else if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE)
|
|
{
|
|
if (pSoldier->aiData.bUnderFire && !pSoldier->aiData.bLastAttackHit && distanceToThreat > reallyClose )
|
|
return SWATTING;
|
|
else
|
|
return RUNNING;
|
|
}
|
|
}
|
|
|
|
// use walking/swatting when flanking in realtime
|
|
if (AICheckIsFlanking(pSoldier) && !gfTurnBasedAI)
|
|
{
|
|
if (NightTime())
|
|
return SWATTING;
|
|
|
|
if (pSoldier->bBreath < pSoldier->bBreathMax / 2)
|
|
return WALKING;
|
|
}
|
|
}
|
|
}
|
|
|
|
return( MovementMode[bAction][Urgency[pSoldier->aiData.bAlertStatus][pSoldier->aiData.bAIMorale]] );
|
|
}
|
|
}
|
|
}
|
|
|
|
void NewDest(SOLDIERTYPE *pSoldier, INT32 usGridNo)
|
|
{
|
|
// sevenfm: always use DetermineMovementMode with new code
|
|
if (gGameExternalOptions.fAIMovementMode)
|
|
{
|
|
pSoldier->usUIMovementMode = DetermineMovementMode(pSoldier, pSoldier->aiData.bAction);
|
|
// check for non merc bodytypes
|
|
if ((pSoldier->usUIMovementMode == SWATTING || pSoldier->usUIMovementMode == SWATTING_WK) && !IS_MERC_BODY_TYPE(pSoldier))
|
|
{
|
|
pSoldier->usUIMovementMode = WALKING;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ATE: Setting sDestination? Tis does not make sense...
|
|
//pSoldier->pathing.sDestination = usGridNo;
|
|
BOOLEAN fSet = FALSE;
|
|
|
|
if (IS_MERC_BODY_TYPE(pSoldier) && pSoldier->aiData.bAction == AI_ACTION_TAKE_COVER && (pSoldier->aiData.bAttitude == DEFENSIVE || pSoldier->aiData.bAttitude == CUNNINGSOLO || pSoldier->aiData.bAttitude == 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->aiData.bAlertStatus][pSoldier->aiData.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->aiData.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->aiData.bAlertStatus == STATUS_RED)
|
|
{
|
|
switch (pSoldier->aiData.bAction)
|
|
{
|
|
case AI_ACTION_MOVE_TO_CLIMB:
|
|
case AI_ACTION_RUN_AWAY:
|
|
pSoldier->usUIMovementMode = DetermineMovementMode(pSoldier, pSoldier->aiData.bAction);
|
|
fSet = TRUE;
|
|
break;
|
|
default:
|
|
if (!fSet)
|
|
{
|
|
pSoldier->usUIMovementMode = DetermineMovementMode(pSoldier, pSoldier->aiData.bAction);
|
|
fSet = TRUE;
|
|
}
|
|
break;
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
pSoldier->usUIMovementMode = DetermineMovementMode(pSoldier, pSoldier->aiData.bAction);
|
|
fSet = TRUE;
|
|
}
|
|
|
|
if (pSoldier->usUIMovementMode == SWATTING && !IS_MERC_BODY_TYPE(pSoldier))
|
|
{
|
|
pSoldier->usUIMovementMode = WALKING;
|
|
}
|
|
}
|
|
}
|
|
|
|
//pSoldier->EVENT_GetNewSoldierPath( pSoldier->pathing.sDestination, pSoldier->usUIMovementMode );
|
|
// ATE: Using this more versatile version
|
|
// Last parameter says whether to re-start the soldier's animation
|
|
// This should be done if buddy was paused for fNoApstofinishMove...
|
|
pSoldier->EVENT_InternalGetNewSoldierPath( usGridNo, pSoldier->usUIMovementMode , FALSE, pSoldier->flags.fNoAPToFinishMove );
|
|
}
|
|
|
|
|
|
BOOLEAN IsActionAffordable(SOLDIERTYPE *pSoldier, INT8 bAction)
|
|
{
|
|
INT16 bMinPointsNeeded = 0;
|
|
|
|
//NumMessage("AffordableAction - Guy#",pSoldier->ubID);
|
|
|
|
if( bAction == AI_ACTION_NONE )
|
|
{
|
|
bAction = pSoldier->aiData.bAction;
|
|
}
|
|
|
|
switch (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 ), GetBasicAPsToPickupItem( pSoldier ) ); // SANDRO
|
|
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 = GetBasicAPsToPickupItem( pSoldier ); // SANDRO
|
|
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->aiData.usActionData,ADDTURNCOST,pSoldier->aiData.bAimTime);
|
|
|
|
#ifdef BETAVERSION
|
|
if (ptsNeeded > pSoldier->bActionPoints)
|
|
{
|
|
/*
|
|
sprintf(tempstr,"AI ERROR: %s has insufficient points for attack action %d at grid %d",
|
|
pSoldier->name,pSoldier->aiData.bAction,pSoldier->aiData.usActionData);
|
|
PopMessage(tempstr);
|
|
*/
|
|
}
|
|
#endif
|
|
break;
|
|
|
|
case AI_ACTION_PULL_TRIGGER: // activate an adjacent panic trigger
|
|
bMinPointsNeeded = APBPConstants[AP_PULL_TRIGGER];
|
|
break;
|
|
|
|
case AI_ACTION_USE_DETONATOR: // grab detonator and set off bomb(s)
|
|
bMinPointsNeeded = APBPConstants[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 = APBPConstants[AP_RADIO];
|
|
break;
|
|
|
|
case AI_ACTION_CHANGE_STANCE: // crouch
|
|
bMinPointsNeeded = GetAPsCrouch(pSoldier, TRUE);
|
|
break;
|
|
|
|
case AI_ACTION_GIVE_AID: // help injured/dying friend
|
|
bMinPointsNeeded = 0;
|
|
break;
|
|
|
|
case AI_ACTION_CLIMB_ROOF:
|
|
{
|
|
INT8 bAPForStandUp = 0;
|
|
INT8 bAPToLookAtWall = (FindDirectionForClimbing( pSoldier, pSoldier->sGridNo, pSoldier->pathing.bLevel ) == pSoldier->ubDirection) ? 0 : GetAPsToLook( pSoldier );
|
|
|
|
// SANDRO - improved this a bit
|
|
if (pSoldier->pathing.bLevel == 0)
|
|
{
|
|
if( PTR_CROUCHED ) bAPForStandUp = (INT8)(GetAPsCrouch(pSoldier, TRUE));
|
|
else if( PTR_PRONE ) bAPForStandUp = GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE);
|
|
bMinPointsNeeded = GetAPsToClimbRoof( pSoldier, FALSE ) + bAPForStandUp + bAPToLookAtWall;
|
|
}
|
|
else
|
|
{
|
|
if( !PTR_CROUCHED ) bAPForStandUp = (INT8)(GetAPsCrouch(pSoldier, TRUE));
|
|
bMinPointsNeeded = GetAPsToClimbRoof( pSoldier, TRUE ) + 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;
|
|
|
|
case AI_ACTION_STEAL_MOVE: // added by SANDRO
|
|
//bMinPointsNeeded = GetAPsToStealItem( pSoldier, NULL, pSoldier->aiData.usActionData );;
|
|
break;
|
|
|
|
case AI_ACTION_JUMP_WINDOW:
|
|
if (!pSoldier->CanClimbWithCurrentBackpack())
|
|
bMinPointsNeeded = GetAPsToJumpThroughWindows( pSoldier, TRUE );
|
|
else
|
|
bMinPointsNeeded = GetAPsToJumpFence( pSoldier, FALSE );
|
|
|
|
break;
|
|
|
|
case AI_ACTION_FREE_PRISONER:
|
|
bMinPointsNeeded = APBPConstants[AP_HANDCUFF];
|
|
break;
|
|
|
|
case AI_ACTION_USE_SKILL:
|
|
bMinPointsNeeded = 10; // TODO
|
|
break;
|
|
|
|
case AI_ACTION_DOCTOR:
|
|
case AI_ACTION_DOCTOR_SELF:
|
|
bMinPointsNeeded = 20; // TODO
|
|
break;
|
|
|
|
case AI_ACTION_SELFDETONATE:
|
|
bMinPointsNeeded = 20; // TODO
|
|
break;
|
|
|
|
default:
|
|
#ifdef BETAVERSION
|
|
//NumMessage("AffordableAction - Illegal action type = ",pSoldier->aiData.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;
|
|
UINT16 ubFriendCount;
|
|
SoldierID ubFriendIDs[MAXMERCS], ubFriendID;
|
|
UINT8 ubDirection;
|
|
UINT8 ubDirsLeft;
|
|
BOOLEAN fDirChecked[8];
|
|
BOOLEAN fFound = FALSE;
|
|
INT32 usDest, usOrigin;
|
|
SOLDIERTYPE * pFriend;
|
|
|
|
|
|
// obtain maximum roaming distance from soldier's origin
|
|
usMaxDist = RoamingRange(pSoldier,&usOrigin);
|
|
|
|
// if range is restricted, make sure origin is a legal gridno!
|
|
if (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->aiData.bNeutral && (pSoldier->bSide == pFriend->bSide)) ||
|
|
(pFriend->aiData.bNeutral && pSoldier->aiData.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 (SpacesAway(usOrigin,Menptr[ubFriendID].sGridNo) - 1 <= usMaxDist)
|
|
{
|
|
// should be close enough, try to find a legal->pathing.sDestination within 1 tile
|
|
|
|
// clear dirChecked flag for all 8 directions
|
|
for ( ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ++ubDirection )
|
|
{
|
|
fDirChecked[ubDirection] = FALSE;
|
|
}
|
|
|
|
ubDirsLeft = NUM_WORLD_DIRECTIONS;
|
|
|
|
// 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
|
|
{
|
|
ubDirection = (UINT8)Random( NUM_WORLD_DIRECTIONS );
|
|
}
|
|
while (fDirChecked[ubDirection]);
|
|
|
|
fDirChecked[ubDirection] = TRUE;
|
|
|
|
// determine the gridno 1 tile away from current friend in this direction
|
|
usDest = NewGridNo(Menptr[ubFriendID].sGridNo,DirectionInc(ubDirection));
|
|
|
|
// if that's out of bounds, ignore it & check next direction
|
|
if (usDest == Menptr[ubFriendID].sGridNo)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (SpacesAway(usOrigin, usDest) > usMaxDist)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!CheckNPCDestination(pSoldier, usDest))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (LegalNPCDestination(pSoldier, usDest, ENSURE_PATH, NOWATER, 0))
|
|
{
|
|
fFound = TRUE; // found a spot
|
|
pSoldier->aiData.usActionData = usDest; // store this->pathing.sDestination
|
|
pSoldier->pathing.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);
|
|
}
|
|
|
|
|
|
INT32 RandDestWithinRange(SOLDIERTYPE *pSoldier)
|
|
{
|
|
INT32 sRandDest = NOWHERE;
|
|
INT32 usOrigin, usMaxDist;
|
|
UINT8 ubTriesLeft;
|
|
BOOLEAN fLimited = FALSE, fFound = FALSE;
|
|
INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXRange, sYRange, sXOffset, sYOffset;
|
|
INT16 sOrigX, sOrigY;
|
|
INT16 sX, sY;
|
|
//DBrot: More Rooms
|
|
//UINT8 ubRoom = 0, ubTempRoom;
|
|
UINT16 usRoom = 0, usTempRoom;
|
|
sOrigX = sOrigY = -1;
|
|
sMaxLeft = sMaxRight = sMaxUp = sMaxDown = sXRange = sYRange = -1;
|
|
|
|
// Try to find a random->pathing.sDestination that's no more than maxDist away from
|
|
// the given gridno of origin
|
|
|
|
if (gfTurnBasedAI)
|
|
{
|
|
ubTriesLeft = 10;
|
|
}
|
|
else
|
|
{
|
|
ubTriesLeft = 1;
|
|
}
|
|
|
|
usMaxDist = RoamingRange(pSoldier,&usOrigin);
|
|
|
|
if ( pSoldier->aiData.bOrders <= CLOSEPATROL && (pSoldier->bTeam == CIV_TEAM || pSoldier->ubProfile != NO_PROFILE ) )
|
|
{
|
|
// any other combo uses the default of ubRoom == 0, set above
|
|
if ( !InARoom( pSoldier->aiData.sPatrolGrid[0], &usRoom ) )
|
|
{
|
|
usRoom = 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,->pathing.sDestination is acceptable
|
|
fFound = TRUE;
|
|
pSoldier->pathing.bPathStored = TRUE; // optimization - Ian
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// keep rolling random->pathing.sDestinations until one's satisfactory or retries used
|
|
while ((ubTriesLeft--) && !fFound)
|
|
{
|
|
if (fLimited)
|
|
{
|
|
UINT8 ubTriesLeft2 = 128;
|
|
do//dnl ch53 111009 This loop should increase search performance, but probably need some counter to prevent eventual endless loop
|
|
{
|
|
sXOffset = ((INT16)Random(sXRange)) - sMaxLeft;
|
|
sYOffset = ((INT16)Random(sYRange)) - sMaxUp;
|
|
sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset);
|
|
}while(!GridNoOnVisibleWorldTile(sRandDest) && --ubTriesLeft2);
|
|
#ifdef BETAVERSION
|
|
if ((sRandDest < 0) || (sRandDest >= GRIDSIZE))
|
|
{
|
|
NumMessage("RandDestWithinRange: ERROR - Gridno out of range! = ",sRandDest);
|
|
sRandDest = random(GRIDSIZE);
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
UINT8 ubTriesLeft2 = 128;
|
|
do//dnl ch53 111009 This loop should increase search performance, but probably need some counter to prevent eventual endless loop
|
|
{
|
|
sRandDest = PreRandom(GRIDSIZE);
|
|
}while(!GridNoOnVisibleWorldTile(sRandDest) && --ubTriesLeft2);
|
|
}
|
|
|
|
if ( usRoom && InARoom( sRandDest, &usTempRoom ) && usTempRoom != usRoom )
|
|
{
|
|
// outside of room available for patrol!
|
|
sRandDest = NOWHERE;
|
|
continue;
|
|
}
|
|
|
|
if (!CheckNPCDestination(pSoldier, sRandDest))
|
|
{
|
|
sRandDest = NOWHERE;
|
|
continue;
|
|
}
|
|
|
|
if (!LegalNPCDestination(pSoldier,sRandDest,ENSURE_PATH,NOWATER,0))
|
|
{
|
|
sRandDest = NOWHERE;
|
|
continue; // try again!
|
|
}
|
|
|
|
// passed all the tests,->pathing.sDestination is acceptable
|
|
fFound = TRUE;
|
|
pSoldier->pathing.bPathStored = TRUE; // optimization - Ian
|
|
}
|
|
}
|
|
|
|
return(sRandDest); // defaults to NOWHERE
|
|
}
|
|
|
|
INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, BOOLEAN * pfChangeLevel )
|
|
{
|
|
INT32 *psLastLoc, *pusNoiseGridNo;
|
|
INT8 *pbLastLevel;
|
|
INT32 sGridNo=-1;
|
|
INT8 bLevel, bClosestLevel = -1;
|
|
BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE;
|
|
INT32 iPathCost;
|
|
INT32 sClosestDisturbance = NOWHERE;
|
|
UINT32 uiLoop;
|
|
UINT32 closestConscious = NOWHERE,closestUnconscious = NOWHERE;
|
|
INT32 iShortestPath = 1000;
|
|
INT32 iShortestPathConscious = 1000,iShortestPathUnconscious = 1000;
|
|
UINT8 *pubNoiseVolume;
|
|
INT8 *pbNoiseLevel;
|
|
INT8 *pbPersOL,*pbPublOL;
|
|
INT32 sClimbGridNo;
|
|
SOLDIERTYPE *pOpponent;
|
|
SOLDIERTYPE *pClosestOpponent = NULL;
|
|
INT32 sDistToEnemy, sDistToClosestEnemy = 10000;
|
|
|
|
// sevenfm: safety check
|
|
if ( pfChangeLevel )
|
|
{
|
|
*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->aiData.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++)
|
|
{
|
|
pOpponent = MercSlots[ uiLoop ];
|
|
|
|
// if this merc is inactive, at base, on assignment, or dead
|
|
if (!pOpponent)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// if this merc is neutral/on same side, he's not an opponent
|
|
if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide) )
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// silversurfer: ignore empty vehicles
|
|
if ( pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpponent->bVehicleID ) == 0 )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID;
|
|
pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID;
|
|
psLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID;
|
|
pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->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) && (pOpponent->stats.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][pOpponent->ubID];
|
|
bLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID];
|
|
}
|
|
|
|
// if we are standing at that gridno (!, obviously our info is old...)
|
|
if (sGridNo == pSoldier->sGridNo)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
{
|
|
// huh?
|
|
continue;
|
|
}
|
|
|
|
// sevenfm: if soldier is zombie and he cannot climb, skip location
|
|
if (pSoldier->IsZombie() && pSoldier->pathing.bLevel != bLevel && !gGameExternalOptions.fZombieCanClimb)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// sevenfm: zombies do not attack vehicles (rftr: or robots)
|
|
if (pSoldier->IsZombie() && (AM_A_ROBOT(pOpponent) || ENEMYROBOT(pOpponent) || ARMED_VEHICLE(pOpponent) || (pOpponent->flags.uiStatusFlags & SOLDIER_VEHICLE)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// sevenfm: when in deep water, skip opponents in deep water
|
|
if (DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel) && DeepWater(sGridNo, bLevel))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// sevenfm: if we found reachable enemy, check other enemies only if they are closer
|
|
sDistToEnemy = PythSpacesAway( pSoldier->sGridNo, sGridNo );
|
|
if (sDistToEnemy < sDistToClosestEnemy || TileIsOutOfBounds(sClosestDisturbance) )
|
|
{
|
|
iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo );
|
|
|
|
// if we can get there and it's first reachable enemy or closer than other known enemies
|
|
if (iPathCost != 0 &&
|
|
(!pClosestOpponent || pClosestOpponent->stats.bLife < OKLIFE || pOpponent->stats.bLife >= OKLIFE) &&
|
|
(TileIsOutOfBounds(sClosestDisturbance) ||
|
|
iPathCost < iShortestPath ||
|
|
pClosestOpponent && !pClosestOpponent->IsZombie() && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE))
|
|
{
|
|
if (fClimbingNecessary)
|
|
{
|
|
sClosestDisturbance = sClimbGridNo;
|
|
}
|
|
else
|
|
{
|
|
sClosestDisturbance = sGridNo;
|
|
}
|
|
|
|
pClosestOpponent = pOpponent;
|
|
sDistToClosestEnemy = sDistToEnemy;
|
|
iShortestPath = iPathCost;
|
|
fClosestClimbingNecessary = fClimbingNecessary;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if any "misc. noise" was also heard recently
|
|
if (!TileIsOutOfBounds(pSoldier->aiData.sNoiseGridno) && pSoldier->aiData.sNoiseGridno != sClosestDisturbance)
|
|
{
|
|
// test this gridno, too
|
|
sGridNo = pSoldier->aiData.sNoiseGridno;
|
|
bLevel = pSoldier->bNoiseLevel;
|
|
|
|
// if we are there (at the noise gridno)
|
|
if (sGridNo == pSoldier->sGridNo)
|
|
{
|
|
for(uiLoop=0; uiLoop<guiNumMercSlots; uiLoop++)//dnl ch58 160813
|
|
{
|
|
pOpponent = MercSlots[uiLoop];
|
|
if(pOpponent && pSoldier->bSide == pOpponent->bSide && pSoldier->ubID != pOpponent->ubID&& pSoldier->aiData.sNoiseGridno == pOpponent->aiData.sNoiseGridno)
|
|
{
|
|
pOpponent->aiData.sNoiseGridno = NOWHERE;// Erase for all from the same team as it not useful anymore, this will avoid others to check already tested location
|
|
pOpponent->aiData.ubNoiseVolume = 0;
|
|
}
|
|
}
|
|
pSoldier->aiData.sNoiseGridno = NOWHERE; // wipe it out, not useful anymore
|
|
pSoldier->aiData.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
|
|
// sevenfm: only if we don't know enemy location or noise source is close and we have not seen enemy recently
|
|
if (iPathCost != 0 &&
|
|
!AICheckIsFlanking(pSoldier) &&
|
|
(TileIsOutOfBounds( sClosestDisturbance ) || iPathCost < iShortestPath && !GuySawEnemy( pSoldier )) )
|
|
{
|
|
if (fClimbingNecessary)
|
|
{
|
|
sClosestDisturbance = sClimbGridNo;
|
|
}
|
|
else
|
|
{
|
|
sClosestDisturbance = sGridNo;
|
|
}
|
|
iShortestPath = iPathCost;
|
|
fClosestClimbingNecessary = fClimbingNecessary;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// if any PUBLIC "misc. noise" was also heard recently
|
|
if (!TileIsOutOfBounds(*pusNoiseGridNo) && *pusNoiseGridNo != sClosestDisturbance )
|
|
{
|
|
// test this gridno, too
|
|
sGridNo = *pusNoiseGridNo;
|
|
bLevel = *pbNoiseLevel;
|
|
|
|
// if we are not NEAR the noise gridno...
|
|
if ( pSoldier->pathing.bLevel != bLevel || PythSpacesAway( pSoldier->sGridNo, sGridNo ) >= 6 || SoldierTo3DLocationLineOfSightTest( pSoldier, sGridNo, bLevel, 0, FALSE, NO_DISTANCE_LIMIT ) == 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
|
|
// sevenfm: only if we don't know enemy location or noise source is close and we have not seen enemy recently
|
|
if (iPathCost != 0 &&
|
|
!AICheckIsFlanking(pSoldier) &&
|
|
(TileIsOutOfBounds(sClosestDisturbance) || iPathCost < iShortestPath && !GuySawEnemy(pSoldier)))
|
|
{
|
|
if (fClimbingNecessary)
|
|
{
|
|
sClosestDisturbance = sClimbGridNo;
|
|
}
|
|
else
|
|
{
|
|
sClosestDisturbance = sGridNo;
|
|
}
|
|
iShortestPath = iPathCost;
|
|
fClosestClimbingNecessary = fClimbingNecessary;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// degrade our public noise a bit
|
|
//dnl ch58 160813
|
|
//*pusNoiseGridNo -= 2;
|
|
if(*pubNoiseVolume > 1)
|
|
(*pubNoiseVolume)--;
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUGDECISIONS
|
|
if (!TileIsOutOfBounds(sClosestDisturbance))
|
|
{
|
|
AINumMessage("CLOSEST DISTURBANCE is at gridno ",sClosestDisturbance);
|
|
}
|
|
#endif
|
|
|
|
// sevenfm: safety check
|
|
if ( pfChangeLevel )
|
|
{
|
|
*pfChangeLevel = fClosestClimbingNecessary;
|
|
}
|
|
|
|
return(sClosestDisturbance);
|
|
}
|
|
|
|
|
|
INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel, SoldierID * pubOpponentID, INT32 * distanceInCellCoords)
|
|
{
|
|
INT32 *psLastLoc,sGridNo, sClosestOpponent = NOWHERE;
|
|
UINT32 uiLoop;
|
|
INT32 iRange, iClosestRange = 1500;
|
|
INT8 *pbPersOL,*pbPublOL;
|
|
INT8 bLevel, bClosestLevel;
|
|
SOLDIERTYPE *pOpponent;
|
|
SOLDIERTYPE *pClosestOpponent = NULL;
|
|
|
|
bClosestLevel = -1;
|
|
|
|
if (pubOpponentID)
|
|
{
|
|
*pubOpponentID = NOBODY;
|
|
}
|
|
|
|
// 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->aiData.bOppList[0];
|
|
pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]);
|
|
|
|
// look through this man's personal & public opplists for opponents known
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
|
|
{
|
|
pOpponent = MercSlots[ uiLoop ];
|
|
|
|
// if this merc is inactive, at base, on assignment, or dead
|
|
if (!pOpponent)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// if this merc is neutral/on same side, he's not an opponent
|
|
if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide))
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// silversurfer: ignore empty vehicles
|
|
if ( pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpponent->bVehicleID ) == 0 )
|
|
continue;
|
|
|
|
// Special stuff for Carmen the bounty hunter
|
|
if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY)
|
|
{
|
|
continue; // next opponent
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID;
|
|
pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID;
|
|
psLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->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][pOpponent->ubID];
|
|
bLevel = gbLastKnownOppLevel[pSoldier->ubID][pOpponent->ubID];
|
|
}
|
|
else
|
|
{
|
|
// using public knowledge, obtain opponent's "best guess" gridno
|
|
sGridNo = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID];
|
|
bLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->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->pathing.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 (sClosestOpponent == NOWHERE ||
|
|
iRange < iClosestRange ||
|
|
pClosestOpponent && !pClosestOpponent->IsZombie() && !BOXER(pSoldier) && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE)
|
|
{
|
|
iClosestRange = iRange;
|
|
sClosestOpponent = sGridNo;
|
|
bClosestLevel = bLevel;
|
|
pClosestOpponent = pOpponent;
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUGDECISIONS
|
|
if (!TileIsOutOfBounds(sClosestOpponent))
|
|
{
|
|
AINumMessage("CLOSEST OPPONENT is at gridno ",sClosestOpponent);
|
|
}
|
|
#endif
|
|
|
|
if (psGridNo)
|
|
{
|
|
*psGridNo = sClosestOpponent;
|
|
}
|
|
if (pbLevel)
|
|
{
|
|
*pbLevel = bClosestLevel;
|
|
}
|
|
if (pubOpponentID && pClosestOpponent)
|
|
{
|
|
*pubOpponentID = pClosestOpponent->ubID;
|
|
}
|
|
if ( distanceInCellCoords )
|
|
{
|
|
*distanceInCellCoords = iClosestRange;
|
|
}
|
|
return( sClosestOpponent );
|
|
}
|
|
|
|
INT32 ClosestSeenOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel)
|
|
{
|
|
INT32 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
|
|
}
|
|
|
|
// silversurfer: ignore empty vehicles
|
|
if ( pOpp->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpp->bVehicleID ) == 0 )
|
|
continue;
|
|
|
|
// Special stuff for Carmen the bounty hunter
|
|
if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpp->ubProfile != SLAY)
|
|
{
|
|
continue; // next opponent
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.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->pathing.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->pathing.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 (!TileIsOutOfBounds(sClosestOpponent))
|
|
{
|
|
AINumMessage("CLOSEST OPPONENT is at gridno ",sClosestOpponent);
|
|
}
|
|
#endif
|
|
|
|
if (psGridNo)
|
|
{
|
|
*psGridNo = sClosestOpponent;
|
|
}
|
|
if (pbLevel)
|
|
{
|
|
*pbLevel = bClosestLevel;
|
|
}
|
|
return( sClosestOpponent );
|
|
}
|
|
|
|
|
|
// special variant with a minor twist
|
|
INT32 ClosestSeenOpponentWithRoof(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel)
|
|
{
|
|
INT32 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
|
|
}
|
|
|
|
// silversurfer: ignore empty vehicles
|
|
if ( pOpp->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpp->bVehicleID ) == 0 )
|
|
continue;
|
|
|
|
// Special stuff for Carmen the bounty hunter
|
|
if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpp->ubProfile != SLAY)
|
|
{
|
|
continue; // next opponent
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.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->pathing.bLevel;
|
|
|
|
// if we are standing at that gridno(!, obviously our info is old...)
|
|
if (sGridNo == pSoldier->sGridNo)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// special: allow zombies to also find opponents on a roof
|
|
// otherwise they have will never decide to climb a roof while they are seeing an enemy
|
|
// 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 ( !pSoldier->IsZombie() && (bLevel != pSoldier->pathing.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 (!TileIsOutOfBounds(sClosestOpponent))
|
|
{
|
|
AINumMessage("CLOSEST OPPONENT is at gridno ",sClosestOpponent);
|
|
}
|
|
#endif
|
|
|
|
if (psGridNo)
|
|
{
|
|
*psGridNo = sClosestOpponent;
|
|
}
|
|
if (pbLevel)
|
|
{
|
|
*pbLevel = bClosestLevel;
|
|
}
|
|
return( sClosestOpponent );
|
|
}
|
|
|
|
INT32 ClosestPC( SOLDIERTYPE *pSoldier, INT32 * psDistance )
|
|
{
|
|
// used by NPCs... find the closest PC
|
|
|
|
// NOTE: skips EPCs!
|
|
|
|
SOLDIERTYPE *pTargetSoldier;
|
|
INT32 sMinDist = WORLD_MAX;
|
|
INT32 sDist;
|
|
INT32 sGridNo = NOWHERE;
|
|
|
|
// Loop through all mercs on player team
|
|
SoldierID ubLoop = gTacticalStatus.Team[ gbPlayerNum ].bFirstID;
|
|
|
|
for ( ; ubLoop <= gTacticalStatus.Team[ gbPlayerNum ].bLastID; ++ubLoop)
|
|
{
|
|
pTargetSoldier = ubLoop;
|
|
|
|
if (!pTargetSoldier->bActive || !pTargetSoldier->bInSector)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// if not conscious, skip him
|
|
if (pTargetSoldier->stats.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->aiData.bOppList[ ubLoop ] != SEEN_CURRENTLY )
|
|
{
|
|
sDist += 10;
|
|
}
|
|
|
|
if (sDist < sMinDist)
|
|
{
|
|
sMinDist = sDist;
|
|
sGridNo = pTargetSoldier->sGridNo;
|
|
}
|
|
}
|
|
|
|
if ( psDistance )
|
|
{
|
|
*psDistance = sMinDist;
|
|
}
|
|
|
|
return( sGridNo );
|
|
}
|
|
|
|
// Flugente: like ClosestPC(...), but does not account for covert or not visible mercs
|
|
INT32 ClosestUnDisguisedPC( SOLDIERTYPE *pSoldier, INT32 * psDistance )
|
|
{
|
|
// used by NPCs... find the closest PC
|
|
// NOTE: skips EPCs!
|
|
|
|
SOLDIERTYPE *pTargetSoldier;
|
|
INT32 sMinDist = WORLD_MAX;
|
|
INT32 sDist;
|
|
INT32 sGridNo = NOWHERE;
|
|
|
|
// Loop through all mercs on player team
|
|
SoldierID ubLoop = gTacticalStatus.Team[ gbPlayerNum ].bFirstID;
|
|
for ( ; ubLoop <= gTacticalStatus.Team[ gbPlayerNum ].bLastID; ++ubLoop )
|
|
{
|
|
pTargetSoldier = ubLoop;
|
|
|
|
if (!pTargetSoldier->bActive || !pTargetSoldier->bInSector)
|
|
continue;
|
|
|
|
// if not conscious, skip him
|
|
if (pTargetSoldier->stats.bLife < OKLIFE)
|
|
continue;
|
|
|
|
if ( AM_AN_EPC( pTargetSoldier ) )
|
|
continue;
|
|
|
|
if ( pTargetSoldier->usSoldierFlagMask & (SOLDIER_COVERT_CIV|SOLDIER_COVERT_SOLDIER) )
|
|
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->aiData.bOppList[ ubLoop ] != SEEN_CURRENTLY )
|
|
continue;
|
|
|
|
if (sDist < sMinDist)
|
|
{
|
|
sMinDist = sDist;
|
|
sGridNo = pTargetSoldier->sGridNo;
|
|
}
|
|
}
|
|
|
|
if ( psDistance )
|
|
{
|
|
*psDistance = sMinDist;
|
|
}
|
|
|
|
return( sGridNo );
|
|
}
|
|
|
|
INT32 FindClosestClimbPointAvailableToAI( SOLDIERTYPE * pSoldier, INT32 sStartGridNo, INT32 sDesiredGridNo, BOOLEAN fClimbUp )
|
|
{
|
|
// sevenfm: safety check
|
|
if (!pSoldier)
|
|
{
|
|
return NOWHERE;
|
|
}
|
|
|
|
// sevenfm: don't check roaming range
|
|
return FindClosestClimbPoint(pSoldier, sStartGridNo, sDesiredGridNo, fClimbUp);
|
|
|
|
/*INT32 sGridNo;
|
|
INT32 sRoamingOrigin;
|
|
INT16 sRoamingRange;
|
|
|
|
if ( pSoldier->flags.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( pSoldier, sStartGridNo, sDesiredGridNo, fClimbUp );
|
|
|
|
|
|
if ( PythSpacesAway( sRoamingOrigin, sGridNo ) + 1 > sRoamingRange )
|
|
{
|
|
return( NOWHERE );
|
|
}
|
|
else
|
|
{
|
|
return( sGridNo );
|
|
}*/
|
|
}
|
|
|
|
BOOLEAN ClimbingNecessary( SOLDIERTYPE * pSoldier, INT32 sDestGridNo, INT8 bDestLevel )
|
|
{
|
|
if (pSoldier->pathing.bLevel == bDestLevel)
|
|
{
|
|
if ( (pSoldier->pathing.bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) )
|
|
{
|
|
return( FALSE );
|
|
}
|
|
else // different buildings!
|
|
{
|
|
return( TRUE );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return( TRUE );
|
|
}
|
|
}
|
|
|
|
INT32 GetInterveningClimbingLocation( SOLDIERTYPE * pSoldier, INT32 sDestGridNo, INT8 bDestLevel, BOOLEAN * pfClimbingNecessary )
|
|
{
|
|
if (pSoldier->pathing.bLevel == bDestLevel)
|
|
{
|
|
if ( (pSoldier->pathing.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->pathing.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, INT32 sDestGridNo, INT8 bDestLevel, BOOLEAN fAddCostAfterClimbingUp, BOOLEAN * pfClimbingNecessary, INT32 * psClimbGridNo )
|
|
{
|
|
INT16 sPathCost;
|
|
INT32 sClimbGridNo;
|
|
|
|
if (pSoldier->pathing.bLevel == bDestLevel)
|
|
{
|
|
if ( (pSoldier->pathing.bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) )
|
|
{
|
|
// on ground or same building... normal!
|
|
sPathCost = EstimatePlotPath( pSoldier, sDestGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0);
|
|
if (pfClimbingNecessary)
|
|
*pfClimbingNecessary = FALSE;
|
|
if (psClimbGridNo)
|
|
*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 (TileIsOutOfBounds(sClimbGridNo))
|
|
{
|
|
sPathCost = 0;
|
|
}
|
|
else
|
|
{
|
|
sPathCost = PlotPath( pSoldier, sClimbGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0 );
|
|
// sevenfm: check if we are already standing at climb gridno
|
|
if (sPathCost != 0 || pSoldier->sGridNo == sClimbGridNo)
|
|
{
|
|
// add in cost of climbing down
|
|
if (fAddCostAfterClimbingUp)
|
|
{
|
|
// add in cost of later climbing up, too
|
|
sPathCost += APBPConstants[AP_CLIMBOFFROOF] + APBPConstants[AP_CLIMBROOF];
|
|
// add in an estimate of getting there after climbing down
|
|
sPathCost += (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) * PythSpacesAway( sClimbGridNo, sDestGridNo );
|
|
}
|
|
else
|
|
{
|
|
sPathCost += APBPConstants[AP_CLIMBOFFROOF];
|
|
// add in an estimate of getting there after climbing down, *but not on top of roof*
|
|
sPathCost += (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) * PythSpacesAway( sClimbGridNo, sDestGridNo ) / 2;
|
|
}
|
|
if (pfClimbingNecessary)
|
|
*pfClimbingNecessary = TRUE;
|
|
if (psClimbGridNo)
|
|
*psClimbGridNo = sClimbGridNo;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// sevenfm: check if zombie cannot climb
|
|
if (pSoldier->IsZombie() && !gGameExternalOptions.fZombieCanClimb)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// different levels
|
|
if (pSoldier->pathing.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 (TileIsOutOfBounds(sClimbGridNo))
|
|
{
|
|
sPathCost = 0;
|
|
}
|
|
else
|
|
{
|
|
sPathCost = PlotPath( pSoldier, sClimbGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0);
|
|
// sevenfm: check if we are already standing at climb gridno
|
|
if (sPathCost != 0 || pSoldier->sGridNo == sClimbGridNo)
|
|
{
|
|
// add in the cost of climbing up or down
|
|
if (pSoldier->pathing.bLevel == 0)
|
|
{
|
|
// must climb up
|
|
sPathCost += APBPConstants[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 += (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) * PythSpacesAway( sClimbGridNo, sDestGridNo );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// must climb down
|
|
sPathCost += APBPConstants[AP_CLIMBOFFROOF];
|
|
// add to path a rough estimate of how far to go from the climb gridno to the friend
|
|
// estimate walk cost
|
|
sPathCost += (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) * PythSpacesAway( sClimbGridNo, sDestGridNo );
|
|
}
|
|
if (pfClimbingNecessary)
|
|
*pfClimbingNecessary = TRUE;
|
|
if (psClimbGridNo)
|
|
*psClimbGridNo = sClimbGridNo;
|
|
}
|
|
}
|
|
}
|
|
|
|
return( sPathCost );
|
|
}
|
|
|
|
BOOLEAN GuySawEnemy( SOLDIERTYPE * pSoldier, UINT8 ubMax )
|
|
{
|
|
UINT8 ubTeamLoop;
|
|
SOLDIERTYPE *pOpponent;
|
|
|
|
for ( ubTeamLoop = 0; ubTeamLoop < MAXTEAMS; ++ubTeamLoop )
|
|
{
|
|
if(!gTacticalStatus.Team[ubTeamLoop].bTeamActive)//dnl ch58 070913 skip any inactive teams
|
|
continue;
|
|
|
|
if ( gTacticalStatus.Team[ ubTeamLoop ].bSide != pSoldier->bSide )
|
|
{
|
|
// consider guys in this team, which isn't on our side
|
|
for ( SoldierID ubIDLoop = gTacticalStatus.Team[ ubTeamLoop ].bFirstID; ubIDLoop <= gTacticalStatus.Team[ ubTeamLoop ].bLastID; ++ubIDLoop )
|
|
{
|
|
pOpponent = ubIDLoop;
|
|
|
|
// if this merc is inactive, at base, on assignment, or dead
|
|
if (!pOpponent)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// if this merc is neutral/on same side, he's not an opponent
|
|
if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// sevenfm: ignore empty vehicles
|
|
if( pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpponent->bVehicleID ) == 0 )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// if this guy SAW an enemy recently...
|
|
if( pSoldier->aiData.bOppList[ ubIDLoop ] >= SEEN_CURRENTLY &&
|
|
pSoldier->aiData.bOppList[ ubIDLoop ] <= ubMax )
|
|
{
|
|
return( TRUE );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return( FALSE );
|
|
}
|
|
|
|
INT32 ClosestReachableFriendInTrouble(SOLDIERTYPE *pSoldier, BOOLEAN * pfClimbingNecessary)
|
|
{
|
|
UINT32 uiLoop;
|
|
INT32 sPathCost, sClosestFriend = NOWHERE, sShortestPath = 1000, sClimbGridNo;
|
|
BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE;
|
|
SOLDIERTYPE *pFriend;
|
|
INT32 sClosestKnownOpponent;
|
|
BOOLEAN fCallHelp;
|
|
|
|
// civilians don't really have any "friends", so they don't bother with this
|
|
if (PTR_CIVILIAN)
|
|
{
|
|
return(NOWHERE);
|
|
}
|
|
|
|
// 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->aiData.bNeutral || (pSoldier->bSide != pFriend->bSide))
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// if this "friend" is actually US
|
|
if (pFriend->ubID == pSoldier->ubID)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
sClosestKnownOpponent = ClosestKnownOpponent(pFriend, NULL, NULL);
|
|
|
|
// CJC: restrict "last one to radio" to only if that guy saw us this turn or last turn
|
|
fCallHelp = FALSE;
|
|
|
|
// if this friend is under fire or he called for help recently
|
|
if (pFriend->aiData.bUnderFire || (pFriend->ubID == gTacticalStatus.Team[pFriend->bTeam].ubLastMercToRadio && GuySawEnemy(pFriend)))
|
|
{
|
|
fCallHelp = TRUE;
|
|
}
|
|
|
|
// zombies always call for help if they know enemy position
|
|
if (pSoldier->IsZombie() && pFriend->IsZombie() && !TileIsOutOfBounds(sClosestKnownOpponent))
|
|
{
|
|
fCallHelp = TRUE;
|
|
}
|
|
|
|
if (!fCallHelp)
|
|
{
|
|
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->pathing.bLevel, TRUE, &fClimbingNecessary, &sClimbGridNo );
|
|
|
|
// if we can get there
|
|
if (sPathCost != 0)
|
|
{
|
|
//sprintf(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 (!TileIsOutOfBounds(sClosestFriend))
|
|
{
|
|
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
|
|
SOLDIERTYPE *pTargetSoldier;
|
|
INT16 sMinDist = 1000;
|
|
INT16 sDist;
|
|
|
|
// Loop through all mercs on player team
|
|
SoldierID ubLoop = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID;
|
|
|
|
for ( ; ubLoop <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID; ++ubLoop )
|
|
{
|
|
if (ubLoop == pSoldier->ubID)
|
|
{
|
|
// same guy - continue!
|
|
continue;
|
|
}
|
|
|
|
pTargetSoldier = ubLoop;
|
|
|
|
if ( pSoldier->bActive && pSoldier->bInSector )
|
|
{
|
|
if (!pTargetSoldier->bActive || !pTargetSoldier->bInSector)
|
|
{
|
|
continue;
|
|
}
|
|
// if not conscious, skip him
|
|
else if (pTargetSoldier->stats.bLife < OKLIFE)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// compare sector #s
|
|
if ( (pSoldier->sSectorX != pTargetSoldier->sSectorX) ||
|
|
(pSoldier->sSectorY != pTargetSoldier->sSectorY) ||
|
|
(pSoldier->bSectorZ != pTargetSoldier->bSectorZ) )
|
|
{
|
|
continue;
|
|
}
|
|
else if (pTargetSoldier->stats.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, INT32 sGridNo )
|
|
{
|
|
if (WaterTooDeepForAttacks( sGridNo, pSoldier->pathing.bLevel ))
|
|
{
|
|
return(TRUE);
|
|
}
|
|
|
|
// smoke
|
|
if (gpWorldLevelData[sGridNo].ubExtFlags[ pSoldier->pathing.bLevel ] & MAPELEMENT_EXT_SMOKE)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return InGas( pSoldier, sGridNo );
|
|
}
|
|
|
|
BOOLEAN InGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo )
|
|
{
|
|
// Armed vehicles and robots do not care about gas or smoke
|
|
if (ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
// smoke
|
|
if ( gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->pathing.bLevel] & (MAPELEMENT_EXT_SMOKE | MAPELEMENT_EXT_SIGNAL_SMOKE | MAPELEMENT_EXT_DEBRIS_SMOKE | MAPELEMENT_EXT_FIRERETARDANT_SMOKE ) )
|
|
return TRUE;
|
|
|
|
return InGas(pSoldier,sGridNo);
|
|
}
|
|
|
|
|
|
INT16 InWaterOrGas(SOLDIERTYPE *pSoldier, INT32 sGridNo)
|
|
{
|
|
if (WaterTooDeepForAttacks( sGridNo, pSoldier->pathing.bLevel ))
|
|
{
|
|
return(TRUE);
|
|
}
|
|
|
|
return (INT16)InGas( pSoldier, sGridNo );
|
|
}
|
|
|
|
BOOLEAN InGasSpot(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bLevel)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
return FALSE;
|
|
|
|
// tear gas
|
|
if ((gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & MAPELEMENT_EXT_TEARGAS) &&
|
|
!DoesSoldierWearGasMask(pSoldier))
|
|
{
|
|
return(TRUE);
|
|
}
|
|
// fire/creature/mustard gas
|
|
// sevenfm: avoid mustard gas even when wearing gas mask
|
|
if (gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_BURNABLEGAS | MAPELEMENT_EXT_CREATUREGAS | MAPELEMENT_EXT_MUSTARDGAS))
|
|
{
|
|
return(TRUE);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN InGas(SOLDIERTYPE *pSoldier, INT32 sGridNo)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
return FALSE;
|
|
|
|
// Armed vehicles and robots do not care about gas or smoke
|
|
if (ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (InGasSpot(pSoldier, sGridNo, pSoldier->pathing.bLevel))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
//WarmSteel - One square away from gas is still considered in gas, because it could expand any moment.
|
|
//Note: this only works for gas that expands with one tile, but hey it's better than nothing!
|
|
int iNeighbourGridNo;
|
|
for (int iDir = 0; iDir < NUM_WORLD_DIRECTIONS; ++iDir)
|
|
{
|
|
iNeighbourGridNo = sGridNo + DirectionInc(iDir);
|
|
|
|
if (!TileIsOutOfBounds(iNeighbourGridNo) && InGasSpot(pSoldier, iNeighbourGridNo, pSoldier->pathing.bLevel))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return(FALSE);
|
|
}
|
|
|
|
BOOLEAN WearGasMaskIfAvailable( SOLDIERTYPE * pSoldier )
|
|
{
|
|
INT8 bSlot, bNewSlot;
|
|
|
|
bSlot = FindGasMask( pSoldier );
|
|
if ( bSlot == NO_SLOT )
|
|
{
|
|
return( FALSE );
|
|
}
|
|
if ( bSlot == HEAD1POS || bSlot == HEAD2POS || bSlot == HELMETPOS )
|
|
{
|
|
return( FALSE );
|
|
}
|
|
if ( pSoldier->inv[ HEAD1POS ].exists() == false )
|
|
{
|
|
bNewSlot = HEAD1POS;
|
|
}
|
|
else if ( pSoldier->inv[ HEAD2POS ].exists() == false )
|
|
{
|
|
bNewSlot = HEAD2POS;
|
|
}
|
|
else
|
|
{
|
|
// screw it, going in position 1 anyhow
|
|
bNewSlot = HEAD1POS;
|
|
}
|
|
|
|
RearrangePocket( pSoldier, bSlot, bNewSlot, TRUE );
|
|
|
|
if ( pSoldier->bTeam == gbPlayerNum )
|
|
{
|
|
pSoldier->DeleteSoldierLight( );
|
|
pSoldier->PositionSoldierLight( );
|
|
}
|
|
|
|
return( TRUE );
|
|
}
|
|
|
|
BOOLEAN InLightAtNight( INT32 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;
|
|
|
|
// sevenfm: use new calculation:
|
|
if (gGameExternalOptions.fAINewMorale)
|
|
{
|
|
return CalcMoraleNew(pSoldier);
|
|
}
|
|
|
|
// 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->stats.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->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY)
|
|
{
|
|
continue; // next opponent
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.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(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->stats.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->aiData.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->aiData.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->aiData.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(tempstr,"Known by friend %s, opplist status %d, percent %d, threat = %d",
|
|
// ExtMen[pFriend->ubID].name,pFriend->aiData.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->aiData.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 || pSoldier->ubSoldierClass == SOLDIER_CLASS_BANDIT)
|
|
{
|
|
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->stats.bLife > 75)
|
|
bMoraleCategory++;
|
|
else
|
|
{
|
|
// if getting a bit low on life
|
|
if (pSoldier->stats.bLife < 40)
|
|
bMoraleCategory--;
|
|
|
|
// if getting REALLY low on life
|
|
if (pSoldier->stats.bLife < 20)
|
|
bMoraleCategory--;
|
|
}
|
|
|
|
|
|
// if soldier is currently not under fire
|
|
if (!pSoldier->aiData.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;
|
|
}
|
|
|
|
// brave guys never get hopeless, at worst they get worried
|
|
// SANDRO - on Insane difficulty enemy morale cannot go below worried
|
|
if (bMoraleCategory == MORALE_HOPELESS)
|
|
{
|
|
if (pSoldier->aiData.bAttitude == BRAVESOLO || pSoldier->aiData.bAttitude == BRAVEAID || zDiffSetting[gGameOptions.ubDifficultyLevel].bEnemyMoraleWorried)
|
|
bMoraleCategory = MORALE_WORRIED;
|
|
}
|
|
|
|
#ifdef DEBUGDECISIONS
|
|
STR tempstr;
|
|
sprintf(tempstr, "Morale = %d (category %d)\n", pSoldier->aiData.bMorale, bMoraleCategory);
|
|
DebugAI(tempstr);
|
|
#endif
|
|
|
|
return(bMoraleCategory);
|
|
}
|
|
|
|
INT32 CalcManThreatValue( SOLDIERTYPE *pEnemy, INT32 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->stats.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) && !BOXER(pEnemy) )
|
|
{
|
|
iThreatValue = -999;
|
|
return( iThreatValue );
|
|
}
|
|
|
|
if (fForCreature)
|
|
{
|
|
// health (1-100)
|
|
iThreatValue += pEnemy->stats.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 += EffectiveExpLevel(pEnemy); // SANDRO - find precise effective exp level
|
|
|
|
// ADD man's total action points (10-35)
|
|
iThreatValue += 25 * pEnemy->CalcActionPoints() / APBPConstants[AP_MAXIMUM];
|
|
|
|
// ADD 1/2 of man's current action points (4-17)
|
|
iThreatValue += 25 * pEnemy->bActionPoints / APBPConstants[AP_MAXIMUM] / 2;
|
|
|
|
// ADD 1/10 of man's current health (0-10)
|
|
iThreatValue += (pEnemy->stats.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->stats.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->aiData.bShock;
|
|
}
|
|
|
|
// if I have a specifically defined spot where I'm at (sometime I don't!)
|
|
if (!TileIsOutOfBounds(sMyGrid))
|
|
{
|
|
// 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->ubDirection == GetDirectionFromCenterCellXYGridNo(pEnemy->sGridNo, sMyGrid))
|
|
{
|
|
iThreatValue += (iThreatValue / 20);
|
|
}
|
|
}
|
|
}
|
|
|
|
// if this man is conscious
|
|
if (pEnemy->stats.bLife >= OKLIFE)
|
|
{
|
|
// and we were told to reduce threat for my cover
|
|
if (ubReduceForCover && (!TileIsOutOfBounds(sMyGrid)))
|
|
{
|
|
// 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->pathing.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->stats.bLife));
|
|
}
|
|
}
|
|
|
|
// threat value of any opponent can never drop below 1
|
|
if (iThreatValue < 1)
|
|
{
|
|
iThreatValue = 1;
|
|
}
|
|
|
|
//sprintf(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(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, INT32 * pusFromGridNo)
|
|
{
|
|
BOOL OppPosKnown = FALSE;
|
|
if (CREATURE_OR_BLOODCAT(pSoldier))
|
|
{
|
|
if (pSoldier->aiData.bAlertStatus > STATUS_YELLOW)
|
|
{
|
|
*pusFromGridNo = pSoldier->sGridNo; // from current position!
|
|
return(MAX_ROAMING_RANGE);
|
|
}
|
|
}
|
|
// sevenfm: no limits for zombies
|
|
if (pSoldier->IsZombie())
|
|
{
|
|
return(MAX_ROAMING_RANGE);
|
|
}
|
|
if (pSoldier->aiData.bOrders == POINTPATROL || pSoldier->aiData.bOrders == RNDPTPATROL)
|
|
{
|
|
// roam near NEXT PATROL POINT, not from where merc starts out
|
|
*pusFromGridNo = pSoldier->aiData.sPatrolGrid[pSoldier->aiData.bNextPatrolPnt];
|
|
}
|
|
else
|
|
{
|
|
// roam around where mercs started
|
|
//*pusFromGridNo = pSoldier->sInitialGridNo;
|
|
*pusFromGridNo = pSoldier->aiData.sPatrolGrid[0];
|
|
}
|
|
|
|
//Do we know about any opponent?
|
|
for (UINT16 oppID = 0; oppID < MAX_NUM_SOLDIERS; oppID++)
|
|
{
|
|
if (pSoldier->aiData.bOppList[oppID] != NOT_HEARD_OR_SEEN && gbPublicOpplist[pSoldier->bTeam][oppID] != NOT_HEARD_OR_SEEN)
|
|
{
|
|
OppPosKnown = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (pSoldier->aiData.bOrders)
|
|
{
|
|
// JA2 GOLD: give non-NPCs a 5 tile roam range for cover in combat when being shot at
|
|
// anv: and tanks who are technically NPCs
|
|
case STATIONARY:
|
|
if ((pSoldier->ubProfile != NO_PROFILE && !ARMED_VEHICLE(pSoldier)) || (pSoldier->aiData.bAlertStatus < STATUS_BLACK && !(pSoldier->aiData.bUnderFire)))
|
|
{
|
|
return(0);
|
|
}
|
|
else
|
|
{
|
|
return(5);
|
|
}
|
|
case ONGUARD:
|
|
return(5);
|
|
case CLOSEPATROL:
|
|
if (pSoldier->aiData.bAlertStatus < STATUS_RED)
|
|
{
|
|
return(5);
|
|
}
|
|
else
|
|
{
|
|
if (!OppPosKnown)
|
|
{
|
|
return(15);
|
|
}
|
|
else
|
|
{
|
|
return(30);
|
|
//return( MAX_ROAMING_RANGE );
|
|
}
|
|
}
|
|
case POINTPATROL:
|
|
if (pSoldier->aiData.bAlertStatus < STATUS_RED)
|
|
{
|
|
return(10);
|
|
}
|
|
else
|
|
{
|
|
if (!OppPosKnown)
|
|
{
|
|
return(20);
|
|
}
|
|
else
|
|
{
|
|
return(40);
|
|
//return( MAX_ROAMING_RANGE );
|
|
}
|
|
} // from nextPatrolGrid, not whereIWas
|
|
case RNDPTPATROL:
|
|
if (pSoldier->aiData.bAlertStatus < STATUS_RED)
|
|
{
|
|
return(10);
|
|
}
|
|
else
|
|
{
|
|
if (!OppPosKnown)
|
|
{
|
|
return(20);
|
|
}
|
|
else
|
|
{
|
|
//return( 40 );
|
|
return(MAX_ROAMING_RANGE);
|
|
}
|
|
}// from nextPatrolGrid, not whereIWas
|
|
case FARPATROL:
|
|
if (pSoldier->aiData.bAlertStatus < STATUS_RED)
|
|
{
|
|
return(15);
|
|
}
|
|
else
|
|
{
|
|
if (!OppPosKnown)
|
|
{
|
|
return(30);
|
|
}
|
|
else
|
|
{
|
|
return(MAX_ROAMING_RANGE);
|
|
}
|
|
}
|
|
case ONCALL:
|
|
if (pSoldier->aiData.bAlertStatus < STATUS_RED)
|
|
{
|
|
return(10);
|
|
}
|
|
else
|
|
{
|
|
if (!OppPosKnown)
|
|
{
|
|
return(30);
|
|
}
|
|
else
|
|
{
|
|
//return(50);
|
|
return(MAX_ROAMING_RANGE);
|
|
}
|
|
}
|
|
case SEEKENEMY: *pusFromGridNo = pSoldier->sGridNo; // from current position!
|
|
return(MAX_ROAMING_RANGE);
|
|
case SNIPER: return ( 5 );
|
|
default:
|
|
#ifdef BETAVERSION
|
|
sprintf(tempstr, "%s has invalid orders = %d", pSoldier->GetName(), pSoldier->aiData.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...
|
|
// 0verhaul: There is now! If not permanent, don't lose weapon ready status because the
|
|
// weapon will be restored after the trial situation is finished.
|
|
//SwapObjs( &(pSoldier->inv[bPocket1]), &(pSoldier->inv[bPocket2]) );
|
|
SwapObjs( pSoldier, bPocket1, bPocket2, bPermanent );
|
|
|
|
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 >= BIGPOCKSTART)
|
|
{
|
|
return( FALSE );
|
|
}
|
|
if (pSoldier->inv[bSlot].exists() == false)
|
|
{
|
|
// well that's just fine then!
|
|
return( TRUE );
|
|
}
|
|
|
|
if(FitsInSmallPocket(&pSoldier->inv[bSlot]) == false)
|
|
{
|
|
// then we're looking for a big pocket
|
|
bSlot = FindEmptySlotWithin( pSoldier, BIGPOCKSTART, MEDPOCKFINAL );
|
|
}
|
|
else
|
|
{
|
|
// try a small pocket first
|
|
bSlot = FindEmptySlotWithin( pSoldier, SMALLPOCKSTART, NUM_INV_SLOTS );
|
|
if (bSlot == NO_SLOT)
|
|
{
|
|
bSlot = FindEmptySlotWithin( pSoldier, BIGPOCKSTART, MEDPOCKFINAL );
|
|
}
|
|
}
|
|
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:
|
|
case SOLDIER_CLASS_BANDIT:
|
|
bDifficulty = bDifficultyBase - 1;
|
|
break;
|
|
|
|
case SOLDIER_CLASS_ARMY:
|
|
bDifficulty = bDifficultyBase;
|
|
break;
|
|
|
|
case SOLDIER_CLASS_ROBOT:
|
|
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;
|
|
|
|
case SOLDIER_CLASS_ZOMBIE:
|
|
bDifficulty = bDifficultyBase;
|
|
break;
|
|
|
|
default:
|
|
if (pSoldier->bTeam == CREATURE_TEAM)
|
|
{
|
|
bDifficulty = bDifficultyBase + pSoldier->pathing.bLevel / 4;
|
|
}
|
|
else // civ...
|
|
{
|
|
bDifficulty = (bDifficultyBase + pSoldier->pathing.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->ubDirection, bNewDirection );
|
|
|
|
for( bLoop = 0; bLoop < 2; bLoop++ )
|
|
{
|
|
fFound = TRUE;
|
|
|
|
bTempDir = pCreature->ubDirection;
|
|
|
|
do
|
|
{
|
|
|
|
bTempDir += bDirChange;
|
|
if (bTempDir < NORTH)
|
|
{
|
|
bTempDir = NORTHWEST;
|
|
}
|
|
else if (bTempDir > NORTHWEST)
|
|
{
|
|
bTempDir = NORTH;
|
|
}
|
|
if (!pCreature->InternalIsValidStance( 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->aiData.bAIMorale - 1;
|
|
|
|
// sevenfm: retreat
|
|
if (pSoldier->RetreatCounterValue() > 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// civilians only run away
|
|
if (pSoldier->aiData.bNeutral)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
INT8 bBonus = -1;
|
|
if (IS_MERC_BODY_TYPE(pSoldier))
|
|
{
|
|
if (!AICheckHasGun(pSoldier))
|
|
bBonus = 2; // if we have no weapons, try to get closer to enemy
|
|
else if (GuySawEnemy(pSoldier, SEEN_LAST_TURN) && AICheckShortWeaponRange(pSoldier))
|
|
bBonus = 1; // bonus if weapon range is short
|
|
}
|
|
|
|
switch (pSoldier->aiData.bAttitude)
|
|
{
|
|
case DEFENSIVE: iRangeFactorMultiplier += max(-1, bBonus); break;
|
|
case BRAVESOLO: iRangeFactorMultiplier += max(2, bBonus); break;
|
|
case BRAVEAID: iRangeFactorMultiplier += max(2, bBonus); break;
|
|
case CUNNINGSOLO: iRangeFactorMultiplier += max(0, bBonus); break;
|
|
case CUNNINGAID: iRangeFactorMultiplier += max(0, bBonus); break;
|
|
case ATTACKSLAYONLY:
|
|
case AGGRESSIVE: iRangeFactorMultiplier += max(1, bBonus); break;
|
|
}
|
|
|
|
if ( gTacticalStatus.bConsNumTurnsWeHaventSeenButEnemyDoes > 0 )
|
|
{
|
|
iRangeFactorMultiplier += gTacticalStatus.bConsNumTurnsWeHaventSeenButEnemyDoes;
|
|
}
|
|
|
|
return iRangeFactorMultiplier;
|
|
}
|
|
|
|
BOOLEAN ArmySeesOpponents( void )
|
|
{
|
|
SOLDIERTYPE * pSoldier;
|
|
|
|
for ( SoldierID cnt = gTacticalStatus.Team[ ENEMY_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ ENEMY_TEAM ].bLastID; ++cnt )
|
|
{
|
|
pSoldier = cnt;
|
|
|
|
if ( pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife >= OKLIFE && pSoldier->aiData.bOppCnt > 0 )
|
|
{
|
|
return( TRUE );
|
|
}
|
|
}
|
|
|
|
return( FALSE );
|
|
}
|
|
|
|
#ifdef DEBUGDECISIONS
|
|
void AIPopMessage ( STR16 str )
|
|
{
|
|
DebugAI(str);
|
|
}
|
|
|
|
void AIPopMessage ( const STR8 str )
|
|
{
|
|
STR tempstr;
|
|
sprintf( tempstr,"%s", str);
|
|
DebugAI(tempstr);
|
|
}
|
|
|
|
void AINumMessage(const STR8 str, INT32 num)
|
|
{
|
|
STR tempstr;
|
|
sprintf( tempstr,"%s %d", str, num);
|
|
DebugAI(tempstr);
|
|
}
|
|
|
|
void AINameMessage(SOLDIERTYPE * pSoldier,const STR8 str,INT32 num)
|
|
{
|
|
STR tempstr;
|
|
sprintf( tempstr,"%d %s %d",pSoldier->GetName() , str, num);
|
|
DebugAI( tempstr );
|
|
}
|
|
#endif
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// HEADROCK:
|
|
//
|
|
// The following function(s) are part of my half-assed attempt to have the AI analyze the tactical
|
|
// situation, by comparing (known) squad sizes, the state of all combatants, and the orders of all
|
|
// friendlies. The idea is to return a value called "TacticalSituation" which can tell a combatant
|
|
// whether he should try to undertake a smarter course of action.
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
/*
|
|
INT16 AssessTacticalSituation( INT8 bTeam )
|
|
{
|
|
UINT16 ubFriendlyTeamTacticalValue = 0;
|
|
UINT16 ubEnemyTeamTacticalValue = 0;
|
|
UINT8 ubSoldierTacticalThreat;
|
|
INT16 ubTacticalSituation;
|
|
UINT16 cnt;
|
|
SOLDIERTYPE * pSoldier;
|
|
|
|
// begin loop through all MERCs.
|
|
for ( cnt = gTacticalStatus.Team[ OUR_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ OUR_TEAM ].bLastID; cnt++ )
|
|
{
|
|
pSoldier = MercPtrs[ cnt ];
|
|
ubSoldierTacticalThreat = CalcStraightThreatValue( pSoldier );
|
|
// Player-controlled Mercs are 1.5 times more threatening than AIs
|
|
if (pSoldier->flags.uiStatusFlags & SOLDIER_PC)
|
|
ubSoldierTacticalThreat = (UINT8)((float)ubSoldierTacticalThreat * 1.5);
|
|
|
|
// Assess Threat
|
|
if (bTeam == OUR_TEAM || bTeam == MILITIA_TEAM)
|
|
{
|
|
// Friendly!
|
|
ubFriendlyTeamTacticalValue += ubSoldierTacticalThreat;
|
|
}
|
|
else
|
|
{
|
|
// Enemy!
|
|
if ( TeamSeesOpponent( ENEMY_TEAM, pSoldier ) )
|
|
ubEnemyTeamTacticalValue += ubSoldierTacticalThreat;
|
|
|
|
}
|
|
}
|
|
|
|
// begin loop through all Militia.
|
|
for ( cnt = gTacticalStatus.Team[ MILITIA_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ MILITIA_TEAM ].bLastID; cnt++ )
|
|
{
|
|
pSoldier = MercPtrs[ cnt ];
|
|
ubSoldierTacticalThreat = CalcStraightThreatValue( pSoldier );
|
|
|
|
// Assess Threat
|
|
if (bTeam == OUR_TEAM || bTeam == MILITIA_TEAM)
|
|
{
|
|
// Friendly!
|
|
ubFriendlyTeamTacticalValue += ubSoldierTacticalThreat;
|
|
}
|
|
else
|
|
{
|
|
// Enemy!
|
|
if ( TeamSeesOpponent( ENEMY_TEAM, pSoldier ) )
|
|
ubEnemyTeamTacticalValue += ubSoldierTacticalThreat;
|
|
}
|
|
}
|
|
|
|
// begin loop through all Enemies.
|
|
for ( cnt = gTacticalStatus.Team[ ENEMY_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ ENEMY_TEAM ].bLastID; cnt++ )
|
|
{
|
|
pSoldier = MercPtrs[ cnt ];
|
|
ubSoldierTacticalThreat = CalcStraightThreatValue( pSoldier );
|
|
|
|
// Assess Threat
|
|
|
|
if (bTeam == ENEMY_TEAM)
|
|
{
|
|
// Friendly!
|
|
ubFriendlyTeamTacticalValue += ubSoldierTacticalThreat;
|
|
}
|
|
else
|
|
{
|
|
// Enemy!
|
|
if ( TeamSeesOpponent( OUR_TEAM, pSoldier ) || TeamSeesOpponent ( MILITIA_TEAM, pSoldier) )
|
|
ubEnemyTeamTacticalValue += ubSoldierTacticalThreat;
|
|
}
|
|
|
|
}
|
|
|
|
ubTacticalSituation = ubEnemyTeamTacticalValue - ubFriendlyTeamTacticalValue;
|
|
|
|
return (ubTacticalSituation);
|
|
|
|
|
|
}
|
|
*/
|
|
|
|
// HEADROCK: Function to check whether a team can see the specified soldier.
|
|
BOOLEAN TeamSeesOpponent( INT8 bTeam, SOLDIERTYPE * pOpponent )
|
|
{
|
|
SOLDIERTYPE * pSoldier;
|
|
SoldierID cnt;
|
|
|
|
// This assertion can be safely removed, assuming the program does what it should. It simply checks
|
|
// whether the "opponent" is on the same team being checked. That should be avoided when calling this
|
|
// function.
|
|
Assert( pOpponent->bTeam != bTeam );
|
|
|
|
// We're checking Merc/Militia visibility
|
|
if (bTeam == OUR_TEAM || bTeam == MILITIA_TEAM )
|
|
{
|
|
for ( cnt = gTacticalStatus.Team[ MILITIA_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ MILITIA_TEAM ].bLastID; ++cnt )
|
|
{
|
|
pSoldier = cnt;
|
|
|
|
if (pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife >= OKLIFE)
|
|
{
|
|
|
|
|
|
if (pSoldier->aiData.bOppList[ pOpponent->ubID ] == SEEN_CURRENTLY)
|
|
return ( TRUE );
|
|
}
|
|
}
|
|
for ( cnt = gTacticalStatus.Team[ OUR_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ OUR_TEAM ].bLastID; ++cnt )
|
|
{
|
|
pSoldier = cnt;
|
|
|
|
if (pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife >= OKLIFE)
|
|
{
|
|
// This assertion can be safely removed, assuming the program does what it should. It simply checks
|
|
// whether the "opponent" is on the same team being checked. That should be avoided when calling this
|
|
// function.
|
|
//Assert( pOpponent->bSide != bSide );
|
|
|
|
if (pSoldier->aiData.bOppList[ pOpponent->ubID ] == SEEN_CURRENTLY)
|
|
return ( TRUE );
|
|
}
|
|
}
|
|
|
|
return ( FALSE );
|
|
}
|
|
// Check enemy visibility
|
|
else if (bTeam == ENEMY_TEAM)
|
|
{
|
|
for ( cnt = gTacticalStatus.Team[ ENEMY_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ ENEMY_TEAM ].bLastID; ++cnt )
|
|
{
|
|
pSoldier = cnt;
|
|
|
|
if (pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife >= OKLIFE)
|
|
{
|
|
// This assertion can be safely removed, assuming the program does what it should. It simply checks
|
|
// whether the "opponent" is on the same team being checked. That should be avoided when calling this
|
|
// function.
|
|
//Assert( pOpponent->bSide != bSide );
|
|
|
|
if (pSoldier->aiData.bOppList[ pOpponent->ubID ] == SEEN_CURRENTLY)
|
|
return ( TRUE );
|
|
}
|
|
}
|
|
return ( FALSE );
|
|
}
|
|
|
|
else
|
|
return (FALSE);
|
|
|
|
}
|
|
|
|
// HEADROCK: Function to assess an enemy's threat value without "me" argument.
|
|
INT32 CalcStraightThreatValue( SOLDIERTYPE *pEnemy )
|
|
{
|
|
INT32 iThreatValue = 0;
|
|
|
|
// If man is inactive, at base, on assignment, dead, unconscious
|
|
if (!pEnemy->bActive || !pEnemy->bInSector || !pEnemy->stats.bLife )
|
|
{
|
|
// he's no threat at all, return a negative number
|
|
iThreatValue = 0;
|
|
return(iThreatValue);
|
|
}
|
|
|
|
else
|
|
{
|
|
// ADD twice the man's level (2-20)
|
|
iThreatValue += EffectiveExpLevel(pEnemy); // SANDRO - find precise effective exp level
|
|
|
|
// ADD man's total action points (10-35)
|
|
iThreatValue += 25 * pEnemy->CalcActionPoints() / APBPConstants[AP_MAXIMUM];
|
|
|
|
// ADD 1/2 of man's current action points (4-17)
|
|
iThreatValue += 25 * pEnemy->bActionPoints / APBPConstants[AP_MAXIMUM] / 2;
|
|
|
|
// ADD 1/10 of man's current health (0-10)
|
|
iThreatValue += (pEnemy->stats.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->stats.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->aiData.bShock;
|
|
}
|
|
|
|
// if this man is conscious
|
|
if (pEnemy->stats.bLife < OKLIFE)
|
|
{
|
|
// 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->stats.bLife));
|
|
}
|
|
}
|
|
|
|
// threat value of any opponent can never drop below 1
|
|
if (iThreatValue < 0)
|
|
{
|
|
iThreatValue = 0;
|
|
}
|
|
|
|
return(iThreatValue);
|
|
}
|
|
|
|
// Flugente: get the id of the closest soldier with a specific flag that we can currently see
|
|
SoldierID GetClosestFlaggedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam, UINT32 aFlag, BOOLEAN fCheckSight )
|
|
{
|
|
SoldierID id = NOBODY;
|
|
SOLDIERTYPE * pFriend;
|
|
INT16 range = aRange;
|
|
|
|
// go through each soldier, looking for "friends" (soldiers on same team)
|
|
for ( SoldierID uiLoop = gTacticalStatus.Team[ auTeam ].bFirstID; uiLoop <= gTacticalStatus.Team[ auTeam ].bLastID; ++uiLoop)
|
|
{
|
|
pFriend = uiLoop;
|
|
|
|
// if this merc is inactive, not in sector, or dead
|
|
if (!pFriend)
|
|
continue;
|
|
|
|
// skip ourselves
|
|
if (pFriend->ubID == pSoldier->ubID)
|
|
continue;
|
|
|
|
// must be on the same level
|
|
if ( pFriend->pathing.bLevel != pSoldier->pathing.bLevel )
|
|
continue;
|
|
|
|
// this is not for tanks
|
|
if ( ARMED_VEHICLE( pFriend ) || ENEMYROBOT( pFriend ))
|
|
continue;
|
|
|
|
// skip if this guy is dead
|
|
if ( pFriend->stats.bLife <= 0 )
|
|
continue;
|
|
|
|
// check for flag
|
|
if ( !(pFriend->usSoldierFlagMask & aFlag) )
|
|
continue;
|
|
|
|
// are we close enough?
|
|
INT16 friendrange = SpacesAway(pSoldier->sGridNo, pFriend->sGridNo);
|
|
if ( friendrange < range)
|
|
{
|
|
// can we see this guy?
|
|
if ( !fCheckSight || SoldierTo3DLocationLineOfSightTest( pSoldier, pFriend->sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) )
|
|
{
|
|
range = friendrange;
|
|
id = pFriend->ubID;
|
|
}
|
|
}
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
// get the id of the closest soldier (closer than x tiles) of a specific team that is wounded that we can currently see
|
|
SoldierID GetClosestWoundedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam )
|
|
{
|
|
SoldierID id = NOBODY;
|
|
SOLDIERTYPE * pFriend;
|
|
INT16 range = aRange;
|
|
|
|
// go through each soldier, looking for "friends" (soldiers on same team)
|
|
for ( SoldierID uiLoop = gTacticalStatus.Team[ auTeam ].bFirstID; uiLoop <= gTacticalStatus.Team[ auTeam ].bLastID; ++uiLoop)
|
|
{
|
|
pFriend = uiLoop;
|
|
|
|
// if this merc is inactive, not in sector, or dead
|
|
if (!pFriend)
|
|
continue;
|
|
|
|
// skip ourselves (if not allowed)
|
|
if ( !gGameExternalOptions.fEnemyMedicsHealSelf && pFriend->ubID == pSoldier->ubID)
|
|
continue;
|
|
|
|
// must be on the same level
|
|
if ( pFriend->pathing.bLevel != pSoldier->pathing.bLevel )
|
|
continue;
|
|
|
|
// this is not for tanks
|
|
if ( ARMED_VEHICLE( pFriend ) || ENEMYROBOT( pFriend ) )
|
|
continue;
|
|
|
|
// skip if this guy is dead, or not wounded (enough)
|
|
if ( pFriend->stats.bLife <= 0 || pFriend->iHealableInjury < gGameExternalOptions.sEnemyMedicsWoundMinAmount )
|
|
continue;
|
|
|
|
// are we close enough?
|
|
INT16 friendrange = SpacesAway(pSoldier->sGridNo, pFriend->sGridNo);
|
|
if ( friendrange < range)
|
|
{
|
|
// can we see this guy?
|
|
//if ( SoldierTo3DLocationLineOfSightTest( pSoldier, pFriend->sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) )
|
|
{
|
|
range = friendrange;
|
|
id = pFriend->ubID;
|
|
}
|
|
}
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
// get the id of the closest medic (closer than x tiles) of a specific team
|
|
SoldierID GetClosestMedicSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam )
|
|
{
|
|
SoldierID id = NOBODY;
|
|
SOLDIERTYPE * pFriend;
|
|
INT16 range = aRange;
|
|
|
|
// go through each soldier, looking for "friends" (soldiers on same team)
|
|
for ( SoldierID uiLoop = gTacticalStatus.Team[ auTeam ].bFirstID; uiLoop <= gTacticalStatus.Team[ auTeam ].bLastID; ++uiLoop)
|
|
{
|
|
pFriend = uiLoop;
|
|
|
|
// if this merc is inactive, not in sector, or dead
|
|
if (!pFriend)
|
|
continue;
|
|
|
|
// skip ourselves (we seek OTHER people)
|
|
if ( pFriend->ubID == pSoldier->ubID)
|
|
continue;
|
|
|
|
// must be on the same level
|
|
if ( pFriend->pathing.bLevel != pSoldier->pathing.bLevel )
|
|
continue;
|
|
|
|
// this is not for tanks
|
|
if ( ARMED_VEHICLE( pFriend ) || ENEMYROBOT( pFriend ) )
|
|
continue;
|
|
|
|
// skip this guy if he is dead or unconscious
|
|
if ( pFriend->stats.bLife < OKLIFE )
|
|
continue;
|
|
|
|
// skip if this guy if he is no medic
|
|
if ( !pFriend->CanMedicAI() )
|
|
continue;
|
|
|
|
// are we close enough?
|
|
INT16 friendrange = SpacesAway(pSoldier->sGridNo, pFriend->sGridNo);
|
|
if ( friendrange < range)
|
|
{
|
|
// can we see this guy?
|
|
//if ( SoldierTo3DLocationLineOfSightTest( pSoldier, pFriend->sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) )
|
|
{
|
|
range = friendrange;
|
|
id = pFriend->ubID;
|
|
}
|
|
}
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
// sevenfm: define normal vision limits for day/night
|
|
INT16 MaxNormalVisionDistance( void )
|
|
{
|
|
if( NightTime() )
|
|
{
|
|
return gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO;
|
|
}
|
|
return gGameExternalOptions.ubStraightSightRange * 2 * STRAIGHT_RATIO;
|
|
}
|
|
|
|
// sevenfm: check friendly soldiers between me and noise gridno
|
|
// count only friends that are active and not stationary/onguard/sniper
|
|
UINT16 CountFriendsInDirection( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo )
|
|
{
|
|
SOLDIERTYPE * pFriend;
|
|
UINT8 ubFriendDir, ubMyDir;
|
|
UINT16 ubFriends = 0;
|
|
|
|
CHECKF(pSoldier);
|
|
|
|
ubMyDir = GetDirectionFromCenterCellXYGridNo(sTargetGridNo, pSoldier->sGridNo);
|
|
|
|
// Run through each friendly.
|
|
for ( SoldierID iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; ++iCounter )
|
|
{
|
|
pFriend = iCounter;
|
|
ubFriendDir = GetDirectionFromCenterCellXYGridNo(sTargetGridNo, pFriend->sGridNo);
|
|
|
|
if (pFriend != pSoldier &&
|
|
pFriend->bActive &&
|
|
pFriend->stats.bLife >= OKLIFE &&
|
|
pFriend->stats.bLife >= pFriend->stats.bLifeMax/2 &&
|
|
pFriend->aiData.bOrders > ONGUARD &&
|
|
pFriend->aiData.bOrders != SNIPER &&
|
|
(ubFriendDir == ubMyDir || ubFriendDir == gOneCDirection[ubMyDir] || ubFriendDir == gOneCCDirection[ubMyDir]) &&
|
|
PythSpacesAway( sTargetGridNo, pFriend->sGridNo) < PythSpacesAway(sTargetGridNo, pSoldier->sGridNo) )
|
|
{
|
|
ubFriends++;
|
|
}
|
|
}
|
|
|
|
return ubFriends;
|
|
}
|
|
|
|
// sevenfm: count nearby friend soldiers
|
|
UINT16 CountNearbyFriends( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance )
|
|
{
|
|
SOLDIERTYPE * pFriend;
|
|
UINT16 ubFriendCount = 0;
|
|
|
|
// safety check
|
|
if( !pSoldier )
|
|
return 0;
|
|
|
|
// Run through each friendly.
|
|
for ( SoldierID iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; ++iCounter )
|
|
{
|
|
pFriend = iCounter;
|
|
// Make sure that character is alive, not too shocked, and conscious, and of higher experience level
|
|
// than the character being suppressed.
|
|
if (pFriend != pSoldier && pFriend->bActive && pFriend->stats.bLife >= OKLIFE &&
|
|
PythSpacesAway( sGridNo, pFriend->sGridNo ) <= ubDistance )
|
|
{
|
|
ubFriendCount++;
|
|
}
|
|
}
|
|
|
|
return ubFriendCount;
|
|
}
|
|
|
|
// sevenfm: new AI morale calculation
|
|
INT8 CalcMoraleNew(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;
|
|
|
|
// zombies always have high morale
|
|
if (pSoldier->IsZombie())
|
|
{
|
|
return MORALE_FEARLESS;
|
|
}
|
|
|
|
// sevenfm: retreat, also when blinded
|
|
if (pSoldier->RetreatCounterValue() > 0 || pSoldier->bBlindedCounter > 0)
|
|
{
|
|
return MORALE_WORRIED;
|
|
}
|
|
|
|
// sevenfm: neutrals always have low AI morale even if they have weapons (so they run from enemy)
|
|
if (pSoldier->aiData.bNeutral)
|
|
{
|
|
return(MORALE_WORRIED);
|
|
}
|
|
|
|
// if army guy has NO weapons left then panic!
|
|
if ( pSoldier->bTeam == ENEMY_TEAM || !pSoldier->aiData.bNeutral )
|
|
{
|
|
if ( FindAIUsableObjClass( pSoldier, IC_WEAPON ) == NO_SLOT )
|
|
{
|
|
// sevenfm: instead of leaving sector, try to attack with hands/knife
|
|
return( MORALE_FEARLESS );
|
|
//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->stats.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->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY)
|
|
{
|
|
continue; // next opponent
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.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(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->stats.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->aiData.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->aiData.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->aiData.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(tempstr,"Known by friend %s, opplist status %d, percent %d, threat = %d",
|
|
// ExtMen[pFriend->ubID].name,pFriend->aiData.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->aiData.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 more aggressive
|
|
if ( pSoldier->ubSoldierClass == SOLDIER_CLASS_ADMINISTRATOR || pSoldier->ubSoldierClass == SOLDIER_CLASS_BANDIT )
|
|
{
|
|
bMoraleCategory += 2;
|
|
}
|
|
|
|
// if have good health
|
|
if( pSoldier->stats.bLife > pSoldier->stats.bLifeMax / 2 )
|
|
{
|
|
bMoraleCategory++;
|
|
}
|
|
// bad health
|
|
if( pSoldier->stats.bLife < pSoldier->stats.bLifeMax / 4 )
|
|
{
|
|
bMoraleCategory--;
|
|
}
|
|
// good breath
|
|
if( pSoldier->bBreath > 50 )
|
|
{
|
|
bMoraleCategory++;
|
|
}
|
|
// bad breath
|
|
if( pSoldier->bBreath < 25 )
|
|
{
|
|
bMoraleCategory--;
|
|
}
|
|
// if not under fire - attack
|
|
if( !pSoldier->aiData.bUnderFire )
|
|
{
|
|
bMoraleCategory++;
|
|
}
|
|
|
|
// count friends that flank around the same spot
|
|
if( CountFriendsFlankSameSpot( pSoldier ) == 0 )
|
|
{
|
|
bMoraleCategory ++;
|
|
}
|
|
|
|
INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL);
|
|
|
|
// if last attack of this soldier hit enemy - increase morale
|
|
if( pSoldier->aiData.bLastAttackHit )
|
|
{
|
|
bMoraleCategory++;
|
|
}
|
|
|
|
// if some friend hit enemy - increase morale
|
|
if (CountNearbyFriendsLastAttackHit(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) > 0)
|
|
{
|
|
bMoraleCategory++;
|
|
}
|
|
|
|
// limit AI morale when soldier is under heavy fire
|
|
/*if (pSoldier->ShockLevelPercent() > 75)
|
|
bMoraleCategory = min(bMoraleCategory, MORALE_NORMAL);
|
|
else if (pSoldier->ShockLevelPercent() > 50)
|
|
bMoraleCategory = min(bMoraleCategory, MORALE_CONFIDENT);*/
|
|
|
|
// limit AI morale depending on morale and shock level
|
|
bMoraleCategory = min(bMoraleCategory, max(MORALE_WORRIED, ((pSoldier->aiData.bOrders == SEEKENEMY ? pSoldier->aiData.bMorale + 20 : pSoldier->aiData.bMorale) * 100 / (100 + pSoldier->ShockLevelPercent())) / 20));
|
|
|
|
// prevent hopeless morale when not under attack
|
|
if (bMoraleCategory == MORALE_HOPELESS && !pSoldier->aiData.bUnderFire)
|
|
{
|
|
bMoraleCategory = MORALE_WORRIED;
|
|
}
|
|
|
|
// check limits
|
|
bMoraleCategory = max(bMoraleCategory, MORALE_HOPELESS);
|
|
bMoraleCategory = min(bMoraleCategory, MORALE_FEARLESS);
|
|
|
|
return(bMoraleCategory);
|
|
}
|
|
|
|
BOOLEAN AICheckSpecialRole(SOLDIERTYPE *pSoldier)
|
|
{
|
|
if (AICheckIsSniper(pSoldier) || AICheckIsMachinegunner(pSoldier) || AICheckIsMortarOperator(pSoldier) || AICheckIsRadioOperator(pSoldier) || AICheckIsCommander(pSoldier))
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN WeAttack(INT8 bTeam)
|
|
{
|
|
if (bTeam >= MAXTEAMS)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (bTeam != ENEMY_TEAM)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
// check that every soldier has SEEKENEMY order
|
|
SOLDIERTYPE * pFriend;
|
|
|
|
// Run through each friendly.
|
|
for ( SoldierID iCounter = gTacticalStatus.Team[bTeam].bFirstID; iCounter <= gTacticalStatus.Team[bTeam].bLastID; ++iCounter )
|
|
{
|
|
pFriend = iCounter;
|
|
|
|
if (pFriend &&
|
|
pFriend->bActive &&
|
|
pFriend->stats.bLife >= OKLIFE &&
|
|
pFriend->aiData.bOrders != SEEKENEMY)
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
UINT8 CountNearbyFriendsLastAttackHit( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance )
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
SOLDIERTYPE * pFriend;
|
|
UINT8 ubFriendCount = 0;
|
|
|
|
// Run through each friendly.
|
|
for ( SoldierID iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; ++iCounter )
|
|
{
|
|
pFriend = iCounter;
|
|
|
|
if (pFriend != pSoldier &&
|
|
pFriend->bActive &&
|
|
pFriend->stats.bLife >= OKLIFE &&
|
|
pFriend->aiData.bOrders > ONGUARD &&
|
|
pFriend->aiData.bOrders != SNIPER &&
|
|
PythSpacesAway( sGridNo, pFriend->sGridNo ) <= ubDistance &&
|
|
pFriend->aiData.bLastAttackHit )
|
|
{
|
|
ubFriendCount++;
|
|
}
|
|
}
|
|
|
|
return ubFriendCount;
|
|
}
|
|
|
|
UINT8 CountFriendsFlankSameSpot(SOLDIERTYPE *pSoldier, INT32 sSpot)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
SOLDIERTYPE * pFriend;
|
|
UINT8 ubFriendCount = 0;
|
|
|
|
UINT8 ubFlankLeft = 0;
|
|
UINT8 ubFlankRight = 0;
|
|
|
|
if (TileIsOutOfBounds(sSpot))
|
|
{
|
|
sSpot = ClosestKnownOpponent(pSoldier, NULL, NULL);
|
|
}
|
|
|
|
if (TileIsOutOfBounds(sSpot))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Run through each friendly.
|
|
for ( SoldierID iCounter = gTacticalStatus.Team[pSoldier->bTeam].bFirstID; iCounter <= gTacticalStatus.Team[pSoldier->bTeam].bLastID; ++iCounter )
|
|
{
|
|
pFriend = iCounter;
|
|
|
|
if (pFriend &&
|
|
pFriend != pSoldier &&
|
|
pFriend->bActive &&
|
|
pFriend->stats.bLife >= OKLIFE &&
|
|
pFriend->aiData.bAlertStatus == STATUS_RED &&
|
|
pFriend->aiData.bOrders > ONGUARD &&
|
|
pFriend->aiData.bOrders != SNIPER)
|
|
{
|
|
// check if this friend flanks around the same spot
|
|
if (pFriend->numFlanks > 0 && pFriend->numFlanks < MAX_FLANKS_RED &&
|
|
PythSpacesAway(pFriend->lastFlankSpot, sSpot) < VISION_RANGE / 2)
|
|
{
|
|
if (pFriend->flags.lastFlankLeft)
|
|
{
|
|
ubFlankLeft++;
|
|
}
|
|
else
|
|
{
|
|
ubFlankRight++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ubFlankLeft + ubFlankRight;
|
|
}
|
|
|
|
// check that soldier is flanking
|
|
BOOLEAN AICheckIsFlanking( SOLDIERTYPE *pSoldier )
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if( pSoldier->aiData.bAlertStatus < STATUS_YELLOW ||
|
|
pSoldier->numFlanks == 0 ||
|
|
pSoldier->numFlanks >= MAX_FLANKS_RED )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// sevenfm: determine minimum flanking directions to stop flanking depending on soldier's attitude
|
|
UINT8 MinFlankDirections( SOLDIERTYPE *pSoldier )
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
switch(pSoldier->aiData.bAttitude)
|
|
{
|
|
case CUNNINGAID:
|
|
case CUNNINGSOLO:
|
|
return 4;
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
// count mobile friends that are in BLACK state and not in a dangerous place or have 3/4 APs or hit enemy recently
|
|
// this is mostly used to check if we can cross dangerous area (in light at night or fresh corpses)
|
|
UINT8 CountFriendsBlack( SOLDIERTYPE *pSoldier, INT32 sClosestOpponent )
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
SOLDIERTYPE * pFriend;
|
|
UINT8 ubFriendCount = 0;
|
|
INT32 sFriendClosestOpponent;
|
|
|
|
// by default, use closest known opponent
|
|
if( sClosestOpponent == NOWHERE )
|
|
{
|
|
sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL );
|
|
}
|
|
|
|
if(TileIsOutOfBounds(sClosestOpponent))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Run through each friendly.
|
|
for ( SoldierID iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; ++iCounter )
|
|
{
|
|
pFriend = iCounter;
|
|
|
|
// Make sure that character is alive, not too shocked, and conscious
|
|
if (pFriend != pSoldier &&
|
|
pFriend->bActive &&
|
|
pFriend->stats.bLife >= OKLIFE)
|
|
{
|
|
//sFriendClosestOpponent = ClosestKnownOpponent( pFriend, NULL, NULL );
|
|
sFriendClosestOpponent = ClosestSeenOpponent( pFriend, NULL, NULL );
|
|
if(!TileIsOutOfBounds(sFriendClosestOpponent) &&
|
|
PythSpacesAway( sClosestOpponent, sFriendClosestOpponent ) < (INT16)TACTICAL_RANGE / 4 &&
|
|
pFriend->aiData.bAlertStatus == STATUS_BLACK &&
|
|
pFriend->stats.bLife > pFriend->stats.bLifeMax / 2 &&
|
|
( GetNearestRottingCorpseAIWarning( pFriend->sGridNo ) == 0 && !InLightAtNight(pFriend->sGridNo, pFriend->pathing.bLevel) ||
|
|
pFriend->bActionPoints > 3*pFriend->bInitialActionPoints/4 ||
|
|
pFriend->aiData.bLastAttackHit )
|
|
)
|
|
{
|
|
ubFriendCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ubFriendCount;
|
|
}
|
|
|
|
// count friends under fire or with shock
|
|
UINT16 CountTeamUnderAttack(INT8 bTeam, INT32 sGridNo, INT16 sDistance)
|
|
{
|
|
SOLDIERTYPE * pFriend;
|
|
UINT16 ubFriendCount = 0;
|
|
|
|
// safety check
|
|
if (bTeam >= MAXTEAMS)
|
|
return 0;
|
|
|
|
// Run through each friendly.
|
|
for ( SoldierID iCounter = gTacticalStatus.Team[bTeam].bFirstID; iCounter <= gTacticalStatus.Team[bTeam].bLastID; ++iCounter )
|
|
{
|
|
pFriend = iCounter;
|
|
|
|
if (pFriend &&
|
|
pFriend->bActive &&
|
|
pFriend->stats.bLife >= OKLIFE &&
|
|
PythSpacesAway(sGridNo, pFriend->sGridNo) <= sDistance &&
|
|
(pFriend->aiData.bUnderFire || pFriend->aiData.bShock > 0))
|
|
{
|
|
ubFriendCount++;
|
|
}
|
|
}
|
|
|
|
return ubFriendCount;
|
|
}
|
|
|
|
// check if we have a prone sight cover from known enemies at spot
|
|
BOOLEAN ProneSightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
UINT32 uiLoop;
|
|
SOLDIERTYPE *pOpponent;
|
|
INT32 *pusLastLoc;
|
|
INT8 *pbPersOL;
|
|
INT8 *pbPublOL;
|
|
INT8 *pbLastLevel;
|
|
|
|
INT32 sThreatLoc;
|
|
//INT32 iThreatCertainty;
|
|
INT8 iThreatLevel;
|
|
|
|
INT32 iDistanceVisible;
|
|
|
|
// look through all opponents for those we know of
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
|
|
{
|
|
pOpponent = MercSlots[ uiLoop ];
|
|
|
|
// if this merc is inactive, at base, on assignment, dead, unconscious
|
|
if (!pOpponent || pOpponent->stats.bLife < OKLIFE)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// if this man is neutral / on the same side, he's not an opponent
|
|
if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide))
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID;
|
|
pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID;
|
|
|
|
pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID;
|
|
pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->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
|
|
sThreatLoc = *pusLastLoc;
|
|
iThreatLevel = *pbLastLevel;
|
|
//iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE];
|
|
}
|
|
else
|
|
{
|
|
// using public knowledge, obtain opponent's "best guess" gridno
|
|
sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID];
|
|
iThreatLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID];
|
|
//iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE];
|
|
}
|
|
|
|
if (TileIsOutOfBounds(sThreatLoc))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (fUnlimited)
|
|
{
|
|
iDistanceVisible = NO_DISTANCE_LIMIT;
|
|
}
|
|
else
|
|
{
|
|
gbForceWeaponReady = true;
|
|
iDistanceVisible = DistanceVisible(pSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, sSpot, pSoldier->pathing.bLevel, pSoldier->IsCowering(), GetPercentTunnelVision(pSoldier));
|
|
gbForceWeaponReady = false;
|
|
//iDistanceVisible = pSoldier->GetMaxDistanceVisible(sSpot, pSoldier->pathing.bLevel, CALC_FROM_WANTED_DIR);
|
|
}
|
|
|
|
if (LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, iDistanceVisible, STANDING_LOS_POS, PRONE_LOS_POS))
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// check if we have a sight cover from known enemies at spot
|
|
BOOLEAN SightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
UINT32 uiLoop;
|
|
SOLDIERTYPE *pOpponent;
|
|
INT32 *pusLastLoc;
|
|
INT8 *pbPersOL;
|
|
INT8 *pbPublOL;
|
|
INT8 *pbLastLevel;
|
|
|
|
INT32 sThreatLoc;
|
|
//INT32 iThreatCertainty;
|
|
INT8 iThreatLevel;
|
|
|
|
INT32 iDistanceVisible;
|
|
|
|
// look through all opponents for those we know of
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
|
|
{
|
|
pOpponent = MercSlots[ uiLoop ];
|
|
|
|
// if this merc is inactive, at base, on assignment, dead, unconscious
|
|
if (!pOpponent || pOpponent->stats.bLife < OKLIFE)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// if this man is neutral / on the same side, he's not an opponent
|
|
if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide))
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID;
|
|
pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID;
|
|
|
|
pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID;
|
|
pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->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
|
|
sThreatLoc = *pusLastLoc;
|
|
iThreatLevel = *pbLastLevel;
|
|
//iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE];
|
|
}
|
|
else
|
|
{
|
|
// using public knowledge, obtain opponent's "best guess" gridno
|
|
sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID];
|
|
iThreatLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID];
|
|
//iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE];
|
|
}
|
|
|
|
if (TileIsOutOfBounds(sThreatLoc))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if(fUnlimited)
|
|
{
|
|
iDistanceVisible = NO_DISTANCE_LIMIT;
|
|
}
|
|
else
|
|
{
|
|
gbForceWeaponReady = true;
|
|
iDistanceVisible = DistanceVisible(pSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, sSpot, pSoldier->pathing.bLevel, pSoldier->IsCowering(), GetPercentTunnelVision(pSoldier));
|
|
gbForceWeaponReady = false;
|
|
//iDistanceVisible = pSoldier->GetMaxDistanceVisible(sSpot, pSoldier->pathing.bLevel, CALC_FROM_WANTED_DIR);
|
|
}
|
|
|
|
if (LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, iDistanceVisible))
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOLEAN EnemySeenSoldierRecently( SOLDIERTYPE *pSoldier, UINT8 ubMax )
|
|
{
|
|
UINT32 uiLoop;
|
|
SOLDIERTYPE *pOpponent;
|
|
|
|
//loop through all the enemies and determine the cover
|
|
for ( uiLoop = 0; uiLoop<guiNumMercSlots; ++uiLoop )
|
|
{
|
|
pOpponent = MercSlots[uiLoop];
|
|
|
|
// if this merc is inactive, at base, on assignment, dead, unconscious
|
|
if ( !pOpponent || pOpponent->stats.bLife < OKLIFE )
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// if this man is neutral / on the same side, he's not an opponent
|
|
if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide) )
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// sevenfm: ignore empty vehicles
|
|
if ( pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpponent->bVehicleID ) == 0 )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// if opponent is collapsed/breath collapsed
|
|
if ( pOpponent->bCollapsed || pOpponent->bBreathCollapsed )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// check that this opponent sees us
|
|
if ( pOpponent->aiData.bOppList[pSoldier->ubID] >= SEEN_CURRENTLY &&
|
|
pOpponent->aiData.bOppList[pSoldier->ubID] <= ubMax )
|
|
{
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
UINT16 CountTeamSeeSoldier( INT8 bTeam, SOLDIERTYPE *pSoldier )
|
|
{
|
|
SOLDIERTYPE *pFriend;
|
|
UINT16 ubFriends = 0;
|
|
|
|
CHECKF( pSoldier );
|
|
|
|
if ( bTeam >= MAXTEAMS )
|
|
return 0;
|
|
|
|
for ( SoldierID cnt = gTacticalStatus.Team[bTeam].bFirstID; cnt <= gTacticalStatus.Team[bTeam].bLastID; ++cnt )
|
|
{
|
|
pFriend = cnt;
|
|
|
|
if ( pFriend->bActive &&
|
|
pFriend->bInSector &&
|
|
pFriend->stats.bLife >= OKLIFE &&
|
|
!pFriend->bCollapsed &&
|
|
!pFriend->bBreathCollapsed )
|
|
{
|
|
if ( pFriend->aiData.bOppList[pSoldier->ubID] == SEEN_CURRENTLY ||
|
|
pFriend->aiData.bOppList[pSoldier->ubID] == SEEN_THIS_TURN )
|
|
{
|
|
++ubFriends;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ubFriends;
|
|
}
|
|
|
|
BOOLEAN CheckDoorAtGridno(UINT32 usGridNo)
|
|
{
|
|
STRUCTURE *pStructure;
|
|
|
|
pStructure = FindStructure(usGridNo, STRUCTURE_ANYDOOR);
|
|
if (pStructure != NULL)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN CheckDoorNearGridno(UINT32 usGridNo)
|
|
{
|
|
UINT8 ubMovementCost;
|
|
INT32 sTempGridNo;
|
|
UINT8 ubDirection;
|
|
|
|
if (CheckDoorAtGridno(usGridNo))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++)
|
|
{
|
|
sTempGridNo = NewGridNo(usGridNo, DirectionInc(ubDirection));
|
|
|
|
if (sTempGridNo != usGridNo)
|
|
{
|
|
ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][0];
|
|
if (IS_TRAVELCOST_DOOR(ubMovementCost))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN FindBombNearby( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance )
|
|
{
|
|
UINT32 uiBombIndex;
|
|
INT32 sCheckGridno;
|
|
|
|
INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset;
|
|
|
|
// determine maximum horizontal limits
|
|
sMaxLeft = min( ubDistance, (sGridNo % MAXCOL));
|
|
sMaxRight = min( ubDistance, MAXCOL - ((sGridNo % MAXCOL) + 1));
|
|
|
|
// determine maximum vertical limits
|
|
sMaxUp = min( ubDistance, (sGridNo / MAXROW));
|
|
sMaxDown = min( ubDistance, MAXROW - ((sGridNo / MAXROW) + 1));
|
|
|
|
for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; ++sYOffset)
|
|
{
|
|
for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; ++sXOffset)
|
|
{
|
|
sCheckGridno = sGridNo + sXOffset + (MAXCOL * sYOffset);
|
|
|
|
if( TileIsOutOfBounds(sCheckGridno) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// search all bombs that we can see
|
|
for (uiBombIndex = 0; uiBombIndex < guiNumWorldBombs; ++uiBombIndex)
|
|
{
|
|
if (gWorldBombs[ uiBombIndex ].fExists &&
|
|
gWorldItems[ gWorldBombs[ uiBombIndex ].iItemIndex ].sGridNo == sCheckGridno &&
|
|
gWorldItems[ gWorldBombs[ uiBombIndex ].iItemIndex ].ubLevel == pSoldier->pathing.bLevel &&
|
|
gWorldItems[ gWorldBombs[ uiBombIndex ].iItemIndex ].bVisible == VISIBLE &&
|
|
gWorldItems[ gWorldBombs[ uiBombIndex ].iItemIndex ].usFlags & WORLD_ITEM_ARMED_BOMB )
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
// danger percent based on distance to closest smoke effect
|
|
UINT8 RedSmokeDanger(INT32 sGridNo, INT8 bLevel)
|
|
{
|
|
UINT32 uiCnt;
|
|
INT32 sDist;
|
|
INT32 sClosestDist;
|
|
INT32 sMaxDist = min(gSkillTraitValues.usVOMortarRadius, TACTICAL_RANGE);
|
|
INT32 sClosestSmoke = NOWHERE;
|
|
UINT8 ubDangerPercent = 0;
|
|
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (!gSkillTraitValues.fROAllowArtillery)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// no artillery strike danger underground
|
|
if (gbWorldSectorZ > 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// check if artillery strike was ordered by any team
|
|
if (!CheckArtilleryStrike())
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// no danger when in a building
|
|
if (bLevel == 0 && CheckRoof(sGridNo))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// deep water should be safe
|
|
if (DeepWater(sGridNo, bLevel))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// no danger when in dense terrain
|
|
if (bLevel == 0 && TerrainDensity(sGridNo, bLevel, 2, FALSE) >= 20)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
//loop through all red smoke effects and find closest
|
|
for (uiCnt = 0; uiCnt < guiNumSmokeEffects; uiCnt++)
|
|
{
|
|
if (gSmokeEffectData[uiCnt].fAllocated &&
|
|
gSmokeEffectData[uiCnt].bType == SIGNAL_SMOKE_EFFECT &&
|
|
!TileIsOutOfBounds(gSmokeEffectData[uiCnt].sGridNo))
|
|
{
|
|
sDist = PythSpacesAway(gSmokeEffectData[uiCnt].sGridNo, sGridNo);
|
|
|
|
if (sClosestSmoke == NOWHERE || sDist < sClosestDist)
|
|
{
|
|
sClosestDist = sDist;
|
|
sClosestSmoke = gSmokeEffectData[uiCnt].sGridNo;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we found red smoke, calculate danger percent based on distance
|
|
// 0% at DAY_VISION_RANGE/2, 100% at zero range
|
|
if (sClosestSmoke != NOWHERE)
|
|
{
|
|
ubDangerPercent = 100 * (sMaxDist - min(sMaxDist, sClosestDist)) / sMaxDist;
|
|
}
|
|
|
|
return ubDangerPercent;
|
|
}
|
|
|
|
// check if artillery strike was ordered by any team
|
|
BOOLEAN CheckArtilleryStrike(void)
|
|
{
|
|
UINT32 uiBombIndex;
|
|
OBJECTTYPE *pObj;
|
|
|
|
// search all bombs
|
|
for (uiBombIndex = 0; uiBombIndex < guiNumWorldBombs; uiBombIndex++)
|
|
{
|
|
if (gWorldBombs[uiBombIndex].fExists &&
|
|
gWorldItems[gWorldBombs[uiBombIndex].iItemIndex].usFlags & WORLD_ITEM_ARMED_BOMB)
|
|
{
|
|
pObj = &(gWorldItems[gWorldBombs[uiBombIndex].iItemIndex].object);
|
|
|
|
if (pObj && pObj->exists() && (*pObj)[0]->data.ubWireNetworkFlag & ANY_ARTILLERY_FLAG)
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN CheckRoof(INT32 sGridNo)
|
|
{
|
|
if (FindStructure(sGridNo, STRUCTURE_ROOF) != NULL)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
BOOLEAN FindNearbyExplosiveStructure(INT32 sSpot, INT8 bLevel)
|
|
{
|
|
INT32 sTempGridNo;
|
|
UINT8 ubDirection;
|
|
|
|
if (TileIsOutOfBounds(sSpot))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// check adjacent reachable tiles
|
|
for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++)
|
|
{
|
|
sTempGridNo = NewGridNo(sSpot, DirectionInc(ubDirection));
|
|
|
|
if (sTempGridNo != sSpot)
|
|
{
|
|
if (FindStructFlag(sTempGridNo, bLevel, STRUCTURE_EXPLOSIVE))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
UINT8 TerrainDensity(INT32 sSpot, INT8 bLevel, UINT8 ubDistance, BOOLEAN fGrass)
|
|
{
|
|
if (TileIsOutOfBounds(sSpot))
|
|
return 0;
|
|
|
|
INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset;
|
|
INT32 sCheckSpot = NOWHERE;
|
|
INT32 sCountSpots = 0;
|
|
INT32 sCountObstacles = 0;
|
|
UINT16 usRoom1, usRoom2;
|
|
|
|
STRUCTURE *pCurrent;
|
|
INT16 sDesiredLevel;
|
|
|
|
// determine maximum horizontal limits
|
|
sMaxLeft = min(ubDistance, (sSpot % MAXCOL));
|
|
sMaxRight = min(ubDistance, MAXCOL - ((sSpot % MAXCOL) + 1));
|
|
|
|
// determine maximum vertical limits
|
|
sMaxUp = min(ubDistance, (sSpot / MAXROW));
|
|
sMaxDown = min(ubDistance, MAXROW - ((sSpot / MAXROW) + 1));
|
|
|
|
// count obstacles
|
|
for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
|
|
{
|
|
for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
|
|
{
|
|
sCheckSpot = sSpot + sXOffset + (MAXCOL * sYOffset);
|
|
|
|
if (TileIsOutOfBounds(sCheckSpot))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (InARoom(sSpot, &usRoom1) != InARoom(sCheckSpot, &usRoom2) || usRoom1 != usRoom2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sCountSpots++;
|
|
|
|
if (!IsLocationSittableExcludingPeople(sCheckSpot, bLevel))
|
|
{
|
|
sCountObstacles++;
|
|
}
|
|
|
|
if (fGrass && IsLocationSittableExcludingPeople(sCheckSpot, bLevel))
|
|
{
|
|
pCurrent = gpWorldLevelData[sCheckSpot].pStructureHead;
|
|
|
|
if (bLevel > 0)
|
|
sDesiredLevel = STRUCTURE_ON_ROOF;
|
|
else
|
|
sDesiredLevel = STRUCTURE_ON_GROUND;
|
|
|
|
if (pCurrent != NULL &&
|
|
pCurrent->sCubeOffset == sDesiredLevel &&
|
|
pCurrent->pDBStructureRef->pDBStructure->ubArmour == 4) // light vegetation
|
|
{
|
|
sCountObstacles++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sCountSpots > 0)
|
|
return 100 * sCountObstacles / sCountSpots;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
INT16 DistanceToClosestActiveOpponent(SOLDIERTYPE *pSoldier, INT32 sSpot)
|
|
{
|
|
INT32 sGridNo;
|
|
UINT32 uiLoop;
|
|
INT8 bLevel;
|
|
SOLDIERTYPE *pOpponent;
|
|
INT16 sDistance, sClosestDistance = -1;
|
|
|
|
if (!pSoldier || TileIsOutOfBounds(sSpot))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// look through this man's personal & public opplists for opponents known
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
|
|
{
|
|
pOpponent = MercSlots[uiLoop];
|
|
|
|
// if this merc is inactive, at base, on assignment, or dead
|
|
if (!pOpponent)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
if (!ValidOpponent(pSoldier, pOpponent))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (pOpponent->stats.bLife < OKLIFE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// if this opponent is unknown personally and publicly
|
|
if (pSoldier->aiData.bOppList[pOpponent->ubID] == NOT_HEARD_OR_SEEN)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// obtain opponent's location and level
|
|
sGridNo = gsLastKnownOppLoc[pSoldier->ubID][pOpponent->ubID];
|
|
bLevel = gbLastKnownOppLevel[pSoldier->ubID][pOpponent->ubID];
|
|
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sDistance = PythSpacesAway(sSpot, sGridNo);
|
|
|
|
if (sClosestDistance < 0 ||
|
|
sDistance < sClosestDistance)
|
|
{
|
|
sClosestDistance = sDistance;
|
|
}
|
|
}
|
|
|
|
return sClosestDistance;
|
|
}
|
|
|
|
BOOLEAN ValidOpponent(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent)
|
|
{
|
|
if (!pSoldier || !pOpponent)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (!pOpponent->bActive ||
|
|
!pOpponent->bInSector ||
|
|
pOpponent->stats.bLife == 0 ||
|
|
CONSIDERED_NEUTRAL(pSoldier, pOpponent) ||
|
|
pSoldier->bSide == pOpponent->bSide ||
|
|
pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY ||
|
|
(pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle(pOpponent->bVehicleID) == 0) ||
|
|
gTacticalStatus.bBoxingState == BOXING && BOXER(pSoldier) && !BOXER(pOpponent) ||
|
|
pOpponent->ubBodyType == CROW)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOLEAN AnyCoverFromSpot( INT32 sSpot, INT8 bLevel, INT32 sThreatLoc, INT8 bThreatLevel )
|
|
{
|
|
UINT8 ubDirection;
|
|
INT32 sCoverSpot;
|
|
INT8 bCoverHeight;
|
|
|
|
if( TileIsOutOfBounds( sSpot ) || TileIsOutOfBounds(sThreatLoc) )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
ubDirection = GetDirectionFromCenterCellXYGridNo(sSpot, sThreatLoc);
|
|
sCoverSpot = NewGridNo( sSpot, DirectionInc( ubDirection ) );
|
|
|
|
if ( TileIsOutOfBounds( sCoverSpot ) )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if ( WhoIsThere2( sCoverSpot, bLevel ) != NOBODY )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if ( IsLocationSittable( sCoverSpot, bLevel ) )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
bCoverHeight = GetTallestStructureHeight( sCoverSpot, bLevel );
|
|
|
|
if ( bCoverHeight >= 2 )
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
UINT16 CountSeenEnemiesLastTurn( SOLDIERTYPE* pSoldier )
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
UINT8 ubTeamLoop;
|
|
UINT16 cnt = 0;
|
|
|
|
for( ubTeamLoop = 0; ubTeamLoop < MAXTEAMS; ubTeamLoop++ )
|
|
{
|
|
if( !gTacticalStatus.Team[ubTeamLoop].bTeamActive )
|
|
continue;
|
|
|
|
if( gTacticalStatus.Team[ ubTeamLoop ].bSide != pSoldier->bSide )
|
|
{
|
|
// consider guys in this team, which isn't on our side
|
|
for( SoldierID ubIDLoop = gTacticalStatus.Team[ ubTeamLoop ].bFirstID; ubIDLoop <= gTacticalStatus.Team[ ubTeamLoop ].bLastID; ++ubIDLoop )
|
|
{
|
|
// if this guy SAW an enemy recently...
|
|
if( pSoldier->aiData.bOppList[ ubIDLoop ] >= SEEN_CURRENTLY &&
|
|
pSoldier->aiData.bOppList[ ubIDLoop ] <= SEEN_LAST_TURN )
|
|
{
|
|
cnt++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return cnt;
|
|
}
|
|
|
|
BOOLEAN NorthSpot(INT32 sSpot, INT8 bLevel)
|
|
{
|
|
if (TileIsOutOfBounds(sSpot))
|
|
return FALSE;
|
|
|
|
if (bLevel > 0)
|
|
return FALSE;
|
|
|
|
INT32 sNextSpot = NewGridNo(sSpot, DirectionInc(NORTHWEST));
|
|
|
|
if (gubWorldMovementCosts[sSpot + DirectionInc(NORTHWEST)][NORTHWEST][0] == TRAVELCOST_OFF_MAP ||
|
|
gubWorldMovementCosts[sNextSpot + DirectionInc(NORTHWEST)][NORTHWEST][0] == TRAVELCOST_OFF_MAP)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
// use soldier AI - merc bodytype, no robots/tanks/boxers/etc
|
|
BOOLEAN SoldierAI(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
BOOLEAN fCivilian = (PTR_CIVILIAN && (pSoldier->ubCivilianGroup == NON_CIV_GROUP ||
|
|
(pSoldier->aiData.bNeutral && gTacticalStatus.fCivGroupHostile[pSoldier->ubCivilianGroup] == CIV_GROUP_NEUTRAL) ||
|
|
(pSoldier->ubBodyType >= FATCIV && pSoldier->ubBodyType <= CRIPPLECIV)));
|
|
|
|
if (!IS_MERC_BODY_TYPE(pSoldier) ||
|
|
pSoldier->aiData.bNeutral ||
|
|
fCivilian ||
|
|
BOXER(pSoldier) ||
|
|
ARMED_VEHICLE(pSoldier) ||
|
|
pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE ||
|
|
AM_A_ROBOT(pSoldier) ||
|
|
ENEMYROBOT(pSoldier))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
UINT8 SpotDangerLevel(SOLDIERTYPE *pSoldier, INT32 sGridNo)
|
|
{
|
|
if (!pSoldier)
|
|
return 0;
|
|
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
return 0;
|
|
|
|
BOOLEAN fAlerted = FALSE;
|
|
BOOLEAN fGreen = FALSE;
|
|
BOOLEAN fProfile = FALSE;
|
|
BOOLEAN fNeutral = FALSE;
|
|
UINT8 ubLevel = 0;
|
|
|
|
if (pSoldier->aiData.bAlertStatus < STATUS_YELLOW)
|
|
fGreen = TRUE;
|
|
|
|
if (pSoldier->ubProfile != NO_PROFILE)
|
|
fProfile = TRUE;
|
|
|
|
if (pSoldier->aiData.bNeutral)
|
|
fNeutral = TRUE;
|
|
|
|
if ((pSoldier->aiData.bAlertStatus >= STATUS_RED || pSoldier->ubSoldierClass == SOLDIER_CLASS_ELITE))
|
|
fAlerted = TRUE;
|
|
|
|
if (!fProfile && !fNeutral && !gGameExternalOptions.fAITacticalRetreat && NorthSpot(sGridNo, pSoldier->pathing.bLevel) ||
|
|
!fProfile && fGreen && CheckDoorNearGridno(sGridNo) ||
|
|
Water(sGridNo, pSoldier->pathing.bLevel) && !pSoldier->IsFlanking() ||
|
|
CorpseWarning(pSoldier, sGridNo, pSoldier->pathing.bLevel))
|
|
ubLevel = 1;
|
|
|
|
if (fAlerted && !fNeutral &&
|
|
(InLightAtNight(sGridNo, pSoldier->pathing.bLevel) || FindNearbyExplosiveStructure(sGridNo, pSoldier->pathing.bLevel)))
|
|
ubLevel = 2;
|
|
|
|
if (DeepWater(sGridNo, pSoldier->pathing.bLevel) && !pSoldier->IsFlanking() ||
|
|
RedSmokeDanger(sGridNo, pSoldier->pathing.bLevel))
|
|
ubLevel = 3;
|
|
|
|
if (InGas(pSoldier, sGridNo) ||
|
|
FindBombNearby(pSoldier, sGridNo, BOMB_DETECTION_RANGE))
|
|
ubLevel = 4;
|
|
|
|
return ubLevel;
|
|
}
|
|
|
|
BOOLEAN CheckNPCDestination(SOLDIERTYPE *pSoldier, INT32 sGridNo)
|
|
{
|
|
if (!pSoldier)
|
|
return FALSE;
|
|
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
return FALSE;
|
|
|
|
// find current danger level
|
|
UINT8 ubLevel = SpotDangerLevel(pSoldier, pSoldier->sGridNo);
|
|
// find danger level at target spot
|
|
UINT8 ubTargetLevel = SpotDangerLevel(pSoldier, sGridNo);
|
|
|
|
// avoid moving into dangerous spot
|
|
if (ubTargetLevel > 0 && ubTargetLevel >= ubLevel)
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOLEAN AllowDeepWaterFlanking(SOLDIERTYPE *pSoldier)
|
|
{
|
|
if (SoldierAI(pSoldier) &&
|
|
pSoldier->bTeam == ENEMY_TEAM &&
|
|
pSoldier->aiData.bOrders == SEEKENEMY &&
|
|
(pSoldier->aiData.bAttitude == CUNNINGSOLO || gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, ATHLETICS_NT)) &&
|
|
pSoldier->aiData.bAlertStatus >= STATUS_RED &&
|
|
!pSoldier->aiData.bUnderFire &&
|
|
!GuySawEnemy(pSoldier))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
UINT8 AIDirection(INT32 sSpot1, INT32 sSpot2)
|
|
{
|
|
if (TileIsOutOfBounds(sSpot1) || TileIsOutOfBounds(sSpot2))
|
|
{
|
|
return DIRECTION_IRRELEVANT;
|
|
}
|
|
|
|
return GetDirectionFromCenterCellXYGridNo(sSpot1, sSpot2);
|
|
}
|
|
|
|
BOOLEAN AICheckIsSniper(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
/*if( AIGunType(pSoldier) != GUN_SN_RIFLE )
|
|
{
|
|
return FALSE;
|
|
}*/
|
|
|
|
if ((AIGunRange(pSoldier) > TACTICAL_RANGE || AICheckHasWeaponOfType(pSoldier, GUN_SN_RIFLE)) &&
|
|
AIGunScoped(pSoldier) &&
|
|
pSoldier->stats.bMarksmanship > 90 &&
|
|
gGameOptions.fNewTraitSystem &&
|
|
HAS_SKILL_TRAIT(pSoldier, SNIPER_NT))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckIsMarksman(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (!AICheckHasGun(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (AIGunType(pSoldier) != GUN_SN_RIFLE &&
|
|
AIGunType(pSoldier) != GUN_RIFLE &&
|
|
AIGunType(pSoldier) != GUN_AS_RIFLE)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (AIGunRange(pSoldier) >= TACTICAL_RANGE &&
|
|
(AIGunScoped(pSoldier) || pSoldier->stats.bMarksmanship > 90 || gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, SNIPER_NT)))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckIsRadioOperator(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (pSoldier->CanUseRadio(FALSE))
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckIsMedic(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (pSoldier->CanMedicAI() && gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, DOCTOR_NT))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
/*if( !HAS_SKILL_TRAIT( pSoldier, DOCTOR_NT) )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if( FindFirstAidKit( pSoldier ) != NO_SLOT ||
|
|
FindMedKit( pSoldier ) != NO_SLOT )
|
|
{
|
|
return TRUE;
|
|
}*/
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckIsMortarOperator(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (pSoldier->HasMortar())
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckIsGLOperator(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
// find GL
|
|
INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_LAUNCHER);
|
|
if (bWeaponIn != NO_SLOT &&
|
|
(EnoughAmmo(pSoldier, FALSE, bWeaponIn) || FindAmmoToReload(pSoldier, bWeaponIn, NO_SLOT) != NO_SLOT))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
// check for attached GL
|
|
INT8 bGunSlot = FindAIUsableObjClass(pSoldier, IC_GUN);
|
|
INT8 bRealWeaponMode = pSoldier->bWeaponMode;
|
|
pSoldier->bWeaponMode = WM_ATTACHED_GL; // So that EnoughAmmo will check for a grenade not a bullet
|
|
if (bGunSlot != NO_SLOT &&
|
|
IsGrenadeLauncherAttached(&pSoldier->inv[bGunSlot]) &&
|
|
(EnoughAmmo(pSoldier, FALSE, bGunSlot) || FindAmmoToReload(pSoldier, bGunSlot, NO_SLOT) != NO_SLOT))
|
|
{
|
|
pSoldier->bWeaponMode = bRealWeaponMode;
|
|
return TRUE;
|
|
}
|
|
pSoldier->bWeaponMode = bRealWeaponMode;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckIsOfficer(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, SQUADLEADER_NT))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckIsCommander(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (gGameOptions.fNewTraitSystem && NUM_SKILL_TRAITS(pSoldier, SQUADLEADER_NT) > 1)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckIsMachinegunner(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!SoldierAI(pSoldier))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (AIGunType(pSoldier) == GUN_LMG)
|
|
{
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AIGunInHandScoped(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (UsingNewCTHSystem() == false && IsScoped(&pSoldier->inv[HANDPOS]))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
if (UsingNewCTHSystem() == true && NCTHIsScoped(&pSoldier->inv[HANDPOS]))
|
|
{
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
// check if gun that AI can use is scoped
|
|
BOOLEAN AIGunScoped(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT8 bWeaponIn;
|
|
|
|
bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN);
|
|
|
|
if (bWeaponIn == NO_SLOT)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (pSoldier->inv[bWeaponIn].exists())
|
|
{
|
|
if (UsingNewCTHSystem())
|
|
{
|
|
return NCTHIsScoped(&pSoldier->inv[bWeaponIn]);
|
|
}
|
|
else
|
|
{
|
|
return IsScoped(&pSoldier->inv[bWeaponIn]);
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
// return range for the AI gun in tiles
|
|
UINT16 AIGunRange(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT8 bWeaponIn;
|
|
|
|
bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN);
|
|
|
|
if (bWeaponIn == NO_SLOT)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (pSoldier->inv[bWeaponIn].exists())
|
|
{
|
|
return GunRange(&pSoldier->inv[bWeaponIn], pSoldier) / CELL_X_SIZE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
UINT16 AIGunClass(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT8 bWeaponIn;
|
|
|
|
bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN);
|
|
|
|
if (bWeaponIn == NO_SLOT)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (pSoldier->inv[bWeaponIn].exists())
|
|
{
|
|
return Weapon[pSoldier->inv[bWeaponIn].usItem].ubWeaponClass;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
UINT16 AIGunType(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT8 bWeaponIn;
|
|
|
|
bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN);
|
|
|
|
if (bWeaponIn == NO_SLOT)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (pSoldier->inv[bWeaponIn].exists())
|
|
{
|
|
return Weapon[pSoldier->inv[bWeaponIn].usItem].ubWeaponType;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
UINT8 AIGunDeadliness(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT8 bWeaponIn;
|
|
|
|
bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN);
|
|
|
|
if (bWeaponIn == NO_SLOT)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (pSoldier->inv[bWeaponIn].exists())
|
|
{
|
|
return Weapon[pSoldier->inv[bWeaponIn].usItem].ubDeadliness;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
UINT16 AIGunAmmo(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN);
|
|
|
|
if (bWeaponIn == NO_SLOT)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (pSoldier->inv[bWeaponIn].exists())
|
|
{
|
|
return pSoldier->inv[bWeaponIn][0]->data.gun.ubGunShotsLeft;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
BOOLEAN AIGunAutofireCapable(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN);
|
|
|
|
if (bWeaponIn == NO_SLOT)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (pSoldier->inv[bWeaponIn].exists())
|
|
{
|
|
return IsGunAutofireCapable(&pSoldier->inv[bWeaponIn]);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN FindObstacleNearSpot(INT32 sSpot, INT8 bLevel)
|
|
{
|
|
if (TileIsOutOfBounds(sSpot))
|
|
{
|
|
return FALSE;
|
|
}
|
|
/*if( !IsLocationSittableExcludingPeople(sSpot, bLevel) )
|
|
{
|
|
return FALSE;
|
|
}*/
|
|
|
|
UINT8 ubMovementCost;
|
|
INT32 sTempGridNo;
|
|
UINT8 ubDirection;
|
|
|
|
// check adjacent reachable tiles
|
|
for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++)
|
|
{
|
|
sTempGridNo = NewGridNo(sSpot, DirectionInc(ubDirection));
|
|
|
|
if (sTempGridNo != sSpot)
|
|
{
|
|
ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel];
|
|
|
|
if (ubMovementCost >= TRAVELCOST_BLOCKED || !IsLocationSittableExcludingPeople(sTempGridNo, bLevel))
|
|
{
|
|
return(TRUE);
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN InSmoke(INT32 sGridNo, INT8 bLevel)
|
|
{
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_SMOKE))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN InSmokeNearby(INT32 sGridNo, INT8 bLevel)
|
|
{
|
|
if (TileIsOutOfBounds(sGridNo))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_SMOKE))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
UINT8 ubDirection;
|
|
INT32 sTempGridNo;
|
|
UINT8 ubMovementCost;
|
|
|
|
// check adjacent reachable tiles
|
|
for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++)
|
|
{
|
|
sTempGridNo = NewGridNo(sGridNo, DirectionInc(ubDirection));
|
|
|
|
if (sTempGridNo != sGridNo)
|
|
{
|
|
ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel];
|
|
|
|
if (ubMovementCost < TRAVELCOST_BLOCKED &&
|
|
gpWorldLevelData[sTempGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_SMOKE))
|
|
{
|
|
return(TRUE);
|
|
}
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN CorpseWarning(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bLevel)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT32 cnt;
|
|
ROTTING_CORPSE *pCorpse;
|
|
UINT8 ubDistance = CORPSE_WARNING_DIST;
|
|
|
|
for (cnt = 0; cnt < giNumRottingCorpse; ++cnt)
|
|
{
|
|
pCorpse = &(gRottingCorpse[cnt]);
|
|
|
|
if (pCorpse &&
|
|
pCorpse->fActivated &&
|
|
pCorpse->def.ubType < ROTTING_STAGE2 &&
|
|
//pCorpse->def.ubType <= FMERC_FALLF &&
|
|
pCorpse->def.ubBodyType <= REGFEMALE &&
|
|
pCorpse->def.ubAIWarningValue > 0 &&
|
|
pCorpse->def.bLevel == bLevel &&
|
|
!TileIsOutOfBounds(pCorpse->def.sGridNo) &&
|
|
PythSpacesAway(sGridNo, pCorpse->def.sGridNo) <= ubDistance &&
|
|
(pSoldier->bTeam == ENEMY_TEAM && CorpseEnemyTeam(pCorpse) || pSoldier->bTeam == MILITIA_TEAM && CorpseMilitiaTeam(pCorpse) || pSoldier->bTeam == CIV_TEAM && !pSoldier->aiData.bNeutral))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN CorpseEnemyTeam(ROTTING_CORPSE *pCorpse)
|
|
{
|
|
CHECKF(pCorpse);
|
|
|
|
// check whether corpse has soldier's uniform
|
|
for (UINT8 i = UNIFORM_ENEMY_ADMIN; i <= UNIFORM_ENEMY_ELITE; ++i)
|
|
{
|
|
if (COMPARE_PALETTEREP_ID(pCorpse->def.VestPal, gUniformColors[i].vest) && COMPARE_PALETTEREP_ID(pCorpse->def.PantsPal, gUniformColors[i].pants))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN CorpseMilitiaTeam(ROTTING_CORPSE *pCorpse)
|
|
{
|
|
CHECKF(pCorpse);
|
|
|
|
// check whether corpse has soldier's uniform
|
|
for (UINT8 i = UNIFORM_MILITIA_ROOKIE; i <= UNIFORM_MILITIA_ELITE; ++i)
|
|
{
|
|
if (COMPARE_PALETTEREP_ID(pCorpse->def.VestPal, gUniformColors[i].vest) && COMPARE_PALETTEREP_ID(pCorpse->def.PantsPal, gUniformColors[i].pants))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
// check that current loaded sector is underground
|
|
BOOLEAN AICheckUnderground(void)
|
|
{
|
|
// not underground
|
|
if (gbWorldSectorZ > 0)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
// check if we have any sight cover from known enemies at spot
|
|
BOOLEAN AnyCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
UINT32 uiLoop;
|
|
SOLDIERTYPE *pOpponent;
|
|
INT32 *pusLastLoc;
|
|
INT8 *pbPersOL;
|
|
INT8 *pbPublOL;
|
|
INT8 *pbLastLevel;
|
|
|
|
INT32 sThreatLoc;
|
|
//INT32 iThreatCertainty;
|
|
INT8 iThreatLevel;
|
|
|
|
// look through all opponents for those we know of
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
|
|
{
|
|
pOpponent = MercSlots[uiLoop];
|
|
|
|
// if this merc is inactive, at base, on assignment, dead, unconscious
|
|
if (!pOpponent || pOpponent->stats.bLife < OKLIFE)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
// if this man is neutral / on the same side, he's not an opponent
|
|
if (CONSIDERED_NEUTRAL(pSoldier, pOpponent) || (pSoldier->bSide == pOpponent->bSide))
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID;
|
|
pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID;
|
|
|
|
pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID;
|
|
pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->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
|
|
// sevenfm: fix for unknown public location
|
|
if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) ||
|
|
(*pbPersOL == *pbPublOL) ||
|
|
*pbPersOL != NOT_HEARD_OR_SEEN && TileIsOutOfBounds(gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]) && !TileIsOutOfBounds(gsLastKnownOppLoc[pSoldier->ubID][pOpponent->ubID]))
|
|
{
|
|
// using personal knowledge, obtain opponent's "best guess" gridno
|
|
sThreatLoc = *pusLastLoc;
|
|
iThreatLevel = *pbLastLevel;
|
|
//iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE];
|
|
}
|
|
else
|
|
{
|
|
// using public knowledge, obtain opponent's "best guess" gridno
|
|
sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID];
|
|
iThreatLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID];
|
|
//iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE];
|
|
}
|
|
|
|
if (!AnyCoverFromSpot(sSpot, pSoldier->pathing.bLevel, sThreatLoc, iThreatLevel) &&
|
|
LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, NO_DISTANCE_LIMIT, STANDING_LOS_POS, PRONE_LOS_POS))
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOLEAN AICheckHasWeaponOfType(SOLDIERTYPE *pSoldier, UINT8 ubWeaponType)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
INT8 invsize = (INT8)pSoldier->inv.size();
|
|
for (INT8 bLoop = 0; bLoop < invsize; ++bLoop)
|
|
{
|
|
if (pSoldier->inv[bLoop].exists() &&
|
|
Item[pSoldier->inv[bLoop].usItem].usItemClass == IC_GUN &&
|
|
Weapon[Item[pSoldier->inv[bLoop].usItem].ubClassIndex].ubWeaponType == ubWeaponType)
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
BOOLEAN AICheckHasGun(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (FindAIUsableObjClass(pSoldier, IC_GUN) != NO_SLOT)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN AICheckShortWeaponRange(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
if (!AICheckHasGun(pSoldier))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
if (AIGunRange(pSoldier) < TACTICAL_RANGE / 2)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN NightLight(void)
|
|
{
|
|
//if( GetTimeOfDayAmbientLightLevel() >= NORMAL_LIGHTLEVEL_DAY + 2 )
|
|
if (gubEnvLightValue >= NORMAL_LIGHTLEVEL_NIGHT - 3)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN DuskLight(void)
|
|
{
|
|
// average between day and night
|
|
if (gubEnvLightValue >= (NORMAL_LIGHTLEVEL_NIGHT + NORMAL_LIGHTLEVEL_DAY) / 2)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
BOOLEAN UsePersonalKnowledge(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID)
|
|
{
|
|
INT8 bPersonalKnowledge;
|
|
INT8 bPublicKnowledge;
|
|
|
|
if (!pSoldier || ubOpponentID == NOBODY)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
bPersonalKnowledge = PersonalKnowledge(pSoldier, ubOpponentID);
|
|
bPublicKnowledge = PublicKnowledge(pSoldier->bTeam, ubOpponentID);
|
|
|
|
if (gubKnowledgeValue[bPublicKnowledge - OLDEST_HEARD_VALUE][bPersonalKnowledge - OLDEST_HEARD_VALUE] > 0 ||
|
|
bPersonalKnowledge != NOT_HEARD_OR_SEEN &&
|
|
TileIsOutOfBounds(KnownPublicLocation(pSoldier->bTeam, ubOpponentID)) &&
|
|
!TileIsOutOfBounds(KnownPersonalLocation(pSoldier, ubOpponentID)))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
INT8 Knowledge(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID)
|
|
{
|
|
if (!pSoldier || ubOpponentID == NOBODY)
|
|
{
|
|
return NOT_HEARD_OR_SEEN;
|
|
}
|
|
|
|
if (UsePersonalKnowledge(pSoldier, ubOpponentID))
|
|
{
|
|
return PersonalKnowledge(pSoldier, ubOpponentID);
|
|
}
|
|
|
|
return PublicKnowledge(pSoldier->bTeam, ubOpponentID);
|
|
}
|
|
|
|
INT32 KnownLocation(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID)
|
|
{
|
|
if (!pSoldier || ubOpponentID == NOBODY)
|
|
{
|
|
return NOWHERE;
|
|
}
|
|
|
|
if (UsePersonalKnowledge(pSoldier, ubOpponentID))
|
|
{
|
|
return KnownPersonalLocation(pSoldier, ubOpponentID);
|
|
}
|
|
|
|
return KnownPublicLocation(pSoldier->bTeam, ubOpponentID);
|
|
}
|
|
|
|
INT8 KnownLevel(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID)
|
|
{
|
|
if (!pSoldier || ubOpponentID == NOBODY)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (UsePersonalKnowledge(pSoldier, ubOpponentID))
|
|
{
|
|
return KnownPersonalLevel(pSoldier, ubOpponentID);
|
|
}
|
|
|
|
return KnownPublicLevel(pSoldier->bTeam, ubOpponentID);
|
|
}
|
|
|
|
INT8 PersonalKnowledge(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID)
|
|
{
|
|
if (!pSoldier || ubOpponentID == NOBODY)
|
|
{
|
|
return NOT_HEARD_OR_SEEN;
|
|
}
|
|
|
|
return pSoldier->aiData.bOppList[ubOpponentID];
|
|
}
|
|
|
|
INT32 KnownPersonalLocation(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID)
|
|
{
|
|
if (!pSoldier || ubOpponentID == NOBODY)
|
|
{
|
|
return NOWHERE;
|
|
}
|
|
/*if(PersonalKnowledge(pSoldier, ubOpponentID) == NOT_HEARD_OR_SEEN)
|
|
{
|
|
return NOWHERE;
|
|
}*/
|
|
|
|
return gsLastKnownOppLoc[pSoldier->ubID][ubOpponentID];
|
|
}
|
|
|
|
INT8 KnownPersonalLevel(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID)
|
|
{
|
|
if (!pSoldier || ubOpponentID == NOBODY)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return gbLastKnownOppLevel[pSoldier->ubID][ubOpponentID];
|
|
}
|
|
|
|
INT8 PublicKnowledge(UINT8 bTeam, SoldierID ubOpponentID)
|
|
{
|
|
if (bTeam >= MAXTEAMS || ubOpponentID == NOBODY)
|
|
{
|
|
return NOT_HEARD_OR_SEEN;
|
|
}
|
|
|
|
return gbPublicOpplist[bTeam][ubOpponentID];
|
|
}
|
|
|
|
INT32 KnownPublicLocation(UINT8 bTeam, SoldierID ubOpponentID)
|
|
{
|
|
if (bTeam >= MAXTEAMS || ubOpponentID == NOBODY)
|
|
{
|
|
return NOWHERE;
|
|
}
|
|
/*if (PublicKnowledge(bTeam, ubOpponentID) == NOT_HEARD_OR_SEEN)
|
|
{
|
|
return NOWHERE;
|
|
}*/
|
|
|
|
return gsPublicLastKnownOppLoc[bTeam][ubOpponentID];
|
|
}
|
|
|
|
INT8 KnownPublicLevel(UINT8 bTeam, SoldierID ubOpponentID)
|
|
{
|
|
if (bTeam >= MAXTEAMS || ubOpponentID == NOBODY)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return gbPublicLastKnownOppLevel[bTeam][ubOpponentID];
|
|
}
|
|
|
|
UINT8 ArmyPercentKilled(void)
|
|
{
|
|
if (gTacticalStatus.Team[ENEMY_TEAM].bMenInSector + gTacticalStatus.ubArmyGuysKilled == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return 100 * gTacticalStatus.ubArmyGuysKilled / (gTacticalStatus.Team[ENEMY_TEAM].bMenInSector + gTacticalStatus.ubArmyGuysKilled);
|
|
}
|
|
|
|
UINT8 TeamPercentKilled(INT8 bTeam)
|
|
{
|
|
if (bTeam == ENEMY_TEAM)
|
|
{
|
|
return ArmyPercentKilled();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
BOOLEAN TeamHighPercentKilled(INT8 bTeam)
|
|
{
|
|
if (bTeam == ENEMY_TEAM && ArmyPercentKilled() > ArmyPercentKilledTolerance())
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
// decide how many soldiers can be killed before alarm will be raised
|
|
UINT8 ArmyPercentKilledTolerance(void)
|
|
{
|
|
// 50% at day, 25% at night, 25-33% for restricted sectors
|
|
return 100 / (2 + SectorCurfew(TRUE));
|
|
}
|
|
|
|
UINT8 SectorCurfew(BOOLEAN fNight)
|
|
{
|
|
UINT8 ubSectorId = SECTOR(gWorldSectorX, gWorldSectorY);
|
|
UINT8 ubSectorData = 0;
|
|
|
|
ubSectorData = SectorExternalData[ubSectorId][gbWorldSectorZ].usCurfewValue;
|
|
|
|
if (fNight && NightLight()) // suspicious at night
|
|
ubSectorData = max(ubSectorData, 1);
|
|
|
|
if (gbWorldSectorZ > 0) // underground we are always suspicious
|
|
ubSectorData = max(ubSectorData, 2);
|
|
|
|
return ubSectorData;
|
|
}
|
|
|
|
INT32 RandomizeLocation(INT32 sSpot, INT8 bLevel, UINT8 ubTimes, SOLDIERTYPE *pSightSoldier)
|
|
{
|
|
if (TileIsOutOfBounds(sSpot))
|
|
{
|
|
return NOWHERE;
|
|
}
|
|
|
|
UINT8 ubDirection;
|
|
UINT8 ubMovementCost;
|
|
INT32 sTempSpot;
|
|
INT32 sSpotArray[NUM_WORLD_DIRECTIONS + 1];
|
|
UINT8 ubSpots;
|
|
|
|
for (UINT8 ubCnt = 0; ubCnt < ubTimes; ubCnt++)
|
|
{
|
|
// store original location
|
|
ubSpots = 1;
|
|
sSpotArray[0] = sSpot;
|
|
|
|
// find adjacent locations
|
|
for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++)
|
|
{
|
|
sTempSpot = NewGridNo(sSpot, DirectionInc(ubDirection));
|
|
|
|
if (sTempSpot != sSpot)
|
|
{
|
|
ubMovementCost = gubWorldMovementCosts[sTempSpot][ubDirection][bLevel];
|
|
|
|
if (ubMovementCost < TRAVELCOST_BLOCKED &&
|
|
IsLocationSittableExcludingPeople(sTempSpot, bLevel) &&
|
|
(!pSightSoldier || SoldierToVirtualSoldierLineOfSightTest(pSightSoldier, sTempSpot, bLevel, ANIM_STAND, TRUE, NO_DISTANCE_LIMIT)))
|
|
{
|
|
sSpotArray[ubSpots] = sTempSpot;
|
|
ubSpots++;
|
|
}
|
|
}
|
|
}
|
|
// find random location
|
|
sSpot = sSpotArray[Random(ubSpots)];
|
|
// stop if could not find any adjacent spot
|
|
if (ubSpots < 2)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return sSpot;
|
|
}
|
|
|
|
INT32 RandomizeOpponentLocation(INT32 sSpot, SOLDIERTYPE *pOpponent, INT16 sMaxDistance)
|
|
{
|
|
if (TileIsOutOfBounds(sSpot))
|
|
{
|
|
return NOWHERE;
|
|
}
|
|
|
|
INT8 bXOffset, bYOffset;
|
|
INT32 sRandomSpot;
|
|
|
|
if (sMaxDistance > 0)
|
|
{
|
|
for (INT cnt = 0; cnt < min(sMaxDistance * 2, 100); cnt++)
|
|
{
|
|
bXOffset = Random(sMaxDistance * 2 + 1) - sMaxDistance;
|
|
bYOffset = Random(sMaxDistance * 2 + 1) - sMaxDistance;
|
|
|
|
sRandomSpot = sSpot + bXOffset + (MAXCOL * bYOffset);
|
|
|
|
if (!TileIsOutOfBounds(sRandomSpot) && NewOKDestination(pOpponent, sRandomSpot, FALSE, pOpponent->pathing.bLevel))
|
|
{
|
|
return sRandomSpot;
|
|
}
|
|
}
|
|
}
|
|
|
|
return sSpot;
|
|
}
|
|
|
|
// first call PrepareThreatlist to make threat list
|
|
SoldierID ClosestKnownThreatID(SOLDIERTYPE *pSoldier, UINT32 uiThreatCnt)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
UINT32 uiLoop;
|
|
INT32 sClosestOpponent = NOWHERE;
|
|
INT32 iRange, iClosestRange;
|
|
SoldierID ubClosestOpponentID = NOBODY;
|
|
|
|
// use global defined threat list
|
|
for (uiLoop = 0; uiLoop < uiThreatCnt; uiLoop++)
|
|
{
|
|
// if for some reason we have incorrect location
|
|
if (TileIsOutOfBounds(Threat[uiLoop].sGridNo))
|
|
continue;
|
|
|
|
iRange = GetRangeInCellCoordsFromGridNoDiff(pSoldier->sGridNo, Threat[uiLoop].sGridNo);
|
|
|
|
if (ubClosestOpponentID == NOBODY || iRange < iClosestRange)
|
|
{
|
|
ubClosestOpponentID = Threat[uiLoop].pOpponent->ubID;
|
|
iClosestRange = iRange;
|
|
sClosestOpponent = Threat[uiLoop].sGridNo;
|
|
}
|
|
}
|
|
|
|
return(ubClosestOpponentID);
|
|
}
|
|
|
|
// first call PrepareThreatlist to make threat list
|
|
SoldierID ClosestSeenThreatID(SOLDIERTYPE *pSoldier, UINT32 uiThreatCnt, UINT8 ubMax)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
UINT32 uiLoop;
|
|
INT32 sClosestOpponent = NOWHERE;
|
|
INT32 iRange, iClosestRange;
|
|
SoldierID ubClosestOpponentID = NOBODY;
|
|
|
|
// use global defined threat list
|
|
for (uiLoop = 0; uiLoop < uiThreatCnt; uiLoop++)
|
|
{
|
|
// if for some reason we have incorrect location
|
|
if (TileIsOutOfBounds(Threat[uiLoop].sGridNo))
|
|
continue;
|
|
|
|
// check knowledge
|
|
if (Threat[uiLoop].bPersonalKnowledge < SEEN_CURRENTLY || Threat[uiLoop].bPersonalKnowledge > ubMax)
|
|
continue;
|
|
|
|
iRange = GetRangeInCellCoordsFromGridNoDiff(pSoldier->sGridNo, Threat[uiLoop].sGridNo);
|
|
|
|
if (ubClosestOpponentID == NOBODY || iRange < iClosestRange)
|
|
{
|
|
ubClosestOpponentID = Threat[uiLoop].pOpponent->ubID;
|
|
iClosestRange = iRange;
|
|
sClosestOpponent = Threat[uiLoop].sGridNo;
|
|
}
|
|
}
|
|
|
|
return(ubClosestOpponentID);
|
|
}
|
|
|
|
UINT32 PrepareThreatlist(SOLDIERTYPE *pSoldier)
|
|
{
|
|
SOLDIERTYPE *pOpponent;
|
|
INT32 iThreatRange, iClosestThreatRange = 1500;
|
|
UINT32 uiLoop;
|
|
INT8 bPersonalKnowledge;
|
|
INT8 bPublicKnowledge;
|
|
INT8 bKnowledge;
|
|
INT32 sThreatLoc;
|
|
INT8 bThreatLevel;
|
|
INT32 iThreatCertainty;
|
|
INT32 iMaxThreatRange = MAX_THREAT_RANGE + AI_PATHCOST_RADIUS;
|
|
|
|
UINT32 uiThreatCnt = 0;
|
|
|
|
if (!pSoldier)
|
|
return 0;
|
|
|
|
// look through all opponents for those we know of
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
|
|
{
|
|
pOpponent = MercSlots[uiLoop];
|
|
|
|
// if this merc is inactive, at base, on assignment, dead, unconscious
|
|
if (!pOpponent || pOpponent->stats.bLife < OKLIFE)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
if (!ValidOpponent(pSoldier, pOpponent))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bKnowledge = Knowledge(pSoldier, pOpponent->ubID);
|
|
bPersonalKnowledge = PersonalKnowledge(pSoldier, pOpponent->ubID);
|
|
bPublicKnowledge = PublicKnowledge(pSoldier->bTeam, pOpponent->ubID);
|
|
|
|
// if this opponent is unknown personally and publicly
|
|
if (bKnowledge == NOT_HEARD_OR_SEEN)
|
|
{
|
|
continue; // next merc
|
|
}
|
|
|
|
sThreatLoc = KnownLocation(pSoldier, pOpponent->ubID);
|
|
bThreatLevel = KnownLevel(pSoldier, pOpponent->ubID);
|
|
iThreatCertainty = ThreatPercent[bKnowledge - OLDEST_HEARD_VALUE];
|
|
|
|
// safety check
|
|
if (TileIsOutOfBounds(sThreatLoc))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// calculate how far away this threat is (in adjusted pixels)
|
|
iThreatRange = GetRangeInCellCoordsFromGridNoDiff(pSoldier->sGridNo, sThreatLoc);
|
|
|
|
// if this opponent is believed to be too far away to really be a threat
|
|
if (iThreatRange > iMaxThreatRange)
|
|
{
|
|
continue; // check next opponent
|
|
}
|
|
|
|
// remember this opponent as a current threat, but DON'T REDUCE FOR COVER!
|
|
Threat[uiThreatCnt].iValue = CalcManThreatValue(pOpponent, pSoldier->sGridNo, FALSE, pSoldier);
|
|
|
|
// if the opponent is no threat at all for some reason
|
|
if (Threat[uiThreatCnt].iValue == -999)
|
|
{
|
|
continue; // check next opponent
|
|
}
|
|
|
|
Threat[uiThreatCnt].pOpponent = pOpponent;
|
|
Threat[uiThreatCnt].sGridNo = sThreatLoc;
|
|
Threat[uiThreatCnt].iCertainty = iThreatCertainty;
|
|
Threat[uiThreatCnt].iOrigRange = iThreatRange;
|
|
|
|
// calculate how many APs he will have at the start of the next turn
|
|
Threat[uiThreatCnt].iAPs = pOpponent->CalcActionPoints();
|
|
|
|
// sevenfm: more information
|
|
Threat[uiThreatCnt].bLevel = bThreatLevel;
|
|
Threat[uiThreatCnt].bKnowledge = bKnowledge;
|
|
Threat[uiThreatCnt].bPersonalKnowledge = bPersonalKnowledge;
|
|
Threat[uiThreatCnt].bPublicKnowledge = bPublicKnowledge;
|
|
|
|
if (iThreatRange < iClosestThreatRange)
|
|
{
|
|
iClosestThreatRange = iThreatRange;
|
|
}
|
|
|
|
uiThreatCnt++;
|
|
}
|
|
|
|
return uiThreatCnt;
|
|
}
|
|
|
|
UINT16 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT16 sDistance)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
UINT32 uiLoop;
|
|
SOLDIERTYPE *pOpponent;
|
|
|
|
INT32 sThreatLoc;
|
|
INT8 iThreatLevel;
|
|
|
|
UINT16 ubNum = 0;
|
|
|
|
// loop through all the enemies
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop)
|
|
{
|
|
pOpponent = MercSlots[uiLoop];
|
|
|
|
// if this merc is inactive, at base, on assignment, dead, unconscious
|
|
if (!pOpponent || pOpponent->stats.bLife < OKLIFE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!ValidOpponent(pSoldier, pOpponent))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// check public knowledge
|
|
if (PublicKnowledge(pSoldier->bTeam, pOpponent->ubID) == NOT_HEARD_OR_SEEN)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sThreatLoc = KnownPublicLocation(pSoldier->bTeam, pOpponent->ubID);
|
|
iThreatLevel = KnownPublicLevel(pSoldier->bTeam, pOpponent->ubID);
|
|
|
|
if (TileIsOutOfBounds(sThreatLoc))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// check distance
|
|
if (PythSpacesAway(sThreatLoc, sGridNo) > sDistance)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ubNum++;
|
|
}
|
|
|
|
return ubNum;
|
|
}
|
|
|
|
UINT16 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier)
|
|
{
|
|
CHECKF(pSoldier);
|
|
|
|
UINT32 uiLoop;
|
|
SOLDIERTYPE *pOpponent;
|
|
|
|
INT32 sThreatLoc;
|
|
INT8 iThreatLevel;
|
|
|
|
UINT16 ubNum = 0;
|
|
|
|
// loop through all the enemies
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop)
|
|
{
|
|
pOpponent = MercSlots[uiLoop];
|
|
|
|
// if this merc is inactive, at base, on assignment, dead, unconscious
|
|
if (!pOpponent || pOpponent->stats.bLife < OKLIFE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!ValidOpponent(pSoldier, pOpponent))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// check public knowledge
|
|
if (PublicKnowledge(pSoldier->bTeam, pOpponent->ubID) == NOT_HEARD_OR_SEEN)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sThreatLoc = KnownPublicLocation(pSoldier->bTeam, pOpponent->ubID);
|
|
iThreatLevel = KnownPublicLevel(pSoldier->bTeam, pOpponent->ubID);
|
|
|
|
if (TileIsOutOfBounds(sThreatLoc))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ubNum++;
|
|
}
|
|
|
|
return ubNum;
|
|
}
|
|
|
|
// sevenfm: check if suppression is possible (count friends in the fire direction)
|
|
BOOLEAN CheckSuppressionDirection(SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, INT8 bTargetLevel)
|
|
{
|
|
SOLDIERTYPE * pFriend;
|
|
UINT8 ubShootingDir;
|
|
UINT32 uiLoop;
|
|
|
|
CHECKF(pSoldier);
|
|
CHECKF(!TileIsOutOfBounds(sTargetGridNo));
|
|
|
|
ubShootingDir = AIDirection(pSoldier->sGridNo, sTargetGridNo);
|
|
|
|
for (uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop)
|
|
{
|
|
pFriend = MercSlots[uiLoop];
|
|
|
|
if (pFriend &&
|
|
pFriend != pSoldier &&
|
|
pFriend->bActive &&
|
|
pFriend->bVisible == TRUE &&
|
|
pFriend->stats.bLife >= OKLIFE &&
|
|
(pFriend->bSide == pSoldier->bSide || CONSIDERED_NEUTRAL(pSoldier, pFriend)) &&
|
|
(pFriend->pathing.bLevel == pSoldier->pathing.bLevel || pFriend->pathing.bLevel == bTargetLevel) &&
|
|
ubShootingDir == AIDirection(pSoldier->sGridNo, pFriend->sGridNo) &&
|
|
PythSpacesAway(pSoldier->sGridNo, pFriend->sGridNo) > 1 &&
|
|
PythSpacesAway(pSoldier->sGridNo, pFriend->sGridNo) < 2 * TACTICAL_RANGE &&
|
|
(gAnimControl[pFriend->usAnimState].ubHeight == ANIM_STAND || gGameExternalOptions.fAllowTargetHeadAndLegIfProne) &&
|
|
//!pFriend->IsCowering() &&
|
|
AISoldierToSoldierChanceToGetThrough(pSoldier, pFriend) > 25)
|
|
//LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, pFriend->sGridNo, pFriend->pathing.bLevel, TRUE, NO_DISTANCE_LIMIT))
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|