Split DecideAction into several specialized DecideAction functions

* Removes the need to constantly check for fCivilian, ARMED_VEHICLE, ENEMY_ROBOT etc inside DecideAction
* Move AI decision checks into their own functions, such as DecideActionRadioRedAlert
* Add new entry to ActionType. AI_ACTION_INVALID signals that no valid action was found when a function returns, or no valid action is possible
* Do a weighted random selection for choosing target under DecideActionBlackSoldier
This commit is contained in:
Asdow
2026-01-19 22:50:27 +02:00
parent c3f4ee754a
commit cd054b549f
16 changed files with 12707 additions and 23 deletions
+684
View File
@@ -4234,3 +4234,687 @@ void CheckTossGrenadeAt(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow, INT32 sTa
pSoldier->bWeaponMode = WM_NORMAL;
}
static void CalculatePossibleShots(SOLDIERTYPE* pSoldier, INT8 gun, std::vector<ATTACKTYPE> &possibleShots)
{
UINT32 uiLoop;
INT32 iAttackValue, iThreatValue, iHitRate, iBestHitRate, iPercentBetter, iEstDamage, iTrueLastTarget;
UINT16 usTrueState, usTurningCost, usRaiseGunCost;
INT16 sMinAPcost;
INT16 sRawAPCost;
INT16 sAimAPCost;
INT16 sBestAPcost;
INT16 sChanceToHit;
INT16 sAimTime;
INT16 sBestAimTime;
INT16 sMaxPossibleAimTime;
UINT8 ubChanceToGetThrough;
UINT8 ubBestChanceToGetThrough;
UINT8 ubFriendlyFireChance;
UINT8 ubBestFriendlyFireChance;
INT16 sBestChanceToHit;
INT16 sStanceAPcost;
BOOLEAN fAddingTurningCost, fAddingRaiseGunCost;
UINT8 ubStance, ubBestStance, ubChanceToReallyHit;
INT8 bScopeMode;
SOLDIERTYPE* pOpponent;
// sevenfm:
BOOLEAN fSuppression = FALSE;
BOOLEAN fReturnFire = FALSE;
INT32 sTarget = NOWHERE;
INT8 bLevel;
INT8 bKnowledge;
INT8 bPersonalKnowledge;
INT8 bPublicKnowledge;
DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "CalcBestShot");
sBestChanceToHit = sBestAimTime = sChanceToHit = ubBestChanceToGetThrough = ubBestFriendlyFireChance = ubChanceToReallyHit = 0;
// sevenfm: set attacking hand and target
pSoldier->ubAttackingHand = HANDPOS;
pSoldier->ubTargetID = NOBODY;
pSoldier->usAttackingWeapon = pSoldier->inv[HANDPOS].usItem;
pSoldier->bWeaponMode = WM_NORMAL;
std::map<INT8, OBJECTTYPE*> ObjList;
GetScopeLists(pSoldier, &pSoldier->inv[HANDPOS], ObjList);
pSoldier->bScopeMode = USE_BEST_SCOPE;
pSoldier->bDoBurst = 0;
pSoldier->bDoAutofire = 0;
// determine which attack against which target has the greatest attack value
for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++)
{
ATTACKTYPE pBestShot;
pBestShot.ubPossible = FALSE;
pBestShot.ubChanceToReallyHit = 0;
pBestShot.iAttackValue = 0;
pBestShot.ubOpponent = NOBODY;
pBestShot.ubFriendlyFireChance = 0;
pBestShot.bWeaponIn = gun;
pOpponent = MercSlots[uiLoop];
fSuppression = FALSE;
fReturnFire = FALSE;
// if this merc is inactive, at base, on assignment, or dead
if (!pOpponent || !pOpponent->stats.bLife)
continue; // next merc
if (!ValidOpponent(pSoldier, pOpponent))
{
continue;
}
// determine return fire
if (pSoldier->aiData.bUnderFire &&
!pSoldier->bBlindedCounter &&
pSoldier->ubPreviousAttackerID == pOpponent->ubID)
{
fReturnFire = TRUE;
}
bKnowledge = Knowledge(pSoldier, pOpponent->ubID);
bPersonalKnowledge = PersonalKnowledge(pSoldier, pOpponent->ubID);
bPublicKnowledge = PublicKnowledge(pSoldier->bTeam, pOpponent->ubID);
// check knowledge
if (bKnowledge != SEEN_CURRENTLY &&
bKnowledge != SEEN_THIS_TURN &&
bKnowledge != SEEN_LAST_TURN &&
bKnowledge != HEARD_THIS_TURN &&
bKnowledge != HEARD_LAST_TURN &&
!((bKnowledge == SEEN_2_TURNS_AGO || bKnowledge == SEEN_3_TURNS_AGO || bKnowledge == HEARD_2_TURNS_AGO) && Weapon[pSoldier->usAttackingWeapon].ubWeaponType == GUN_LMG))
{
continue; // next opponent
}
// sevenfm: blind soldier can only attack seen/heard personally
if (pSoldier->bBlindedCounter > 0 &&
bPersonalKnowledge != SEEN_THIS_TURN &&
bPersonalKnowledge != HEARD_THIS_TURN)
{
continue; // next opponent
}
// sevenfm: determine if we shoot on unseen target for suppression
if (bPersonalKnowledge != SEEN_CURRENTLY &&
bPublicKnowledge != SEEN_CURRENTLY &&
//!SoldierToSoldierLineOfSightTest(pSoldier, pOpponent, TRUE, CALC_FROM_ALL_DIRS))
!LOS_Raised(pSoldier, pOpponent, CALC_FROM_ALL_DIRS))
{
fSuppression = TRUE;
}
// sevenfm: shooting at unseen opponents is optional
if (fSuppression && !gGameExternalOptions.fAIShootUnseen)
{
continue; // next opponent
}
// determine enemy location
if (fSuppression)
{
// personal/public knowledge
sTarget = KnownLocation(pSoldier, pOpponent->ubID);
bLevel = KnownLevel(pSoldier, pOpponent->ubID);
// try to randomize location
sTarget = RandomizeLocation(sTarget, bLevel, 1, pSoldier);
}
else
{
// we know exact enemy location
sTarget = pOpponent->sGridNo;
bLevel = pOpponent->pathing.bLevel;
}
// safety check
if (TileIsOutOfBounds(sTarget))
{
continue;
}
// skip if we can see location and location is empty
if (SoldierToVirtualSoldierLineOfSightTest(pSoldier, sTarget, bLevel, ANIM_PRONE, TRUE, CALC_FROM_ALL_DIRS) &&
WhoIsThere2(sTarget, bLevel) == NOBODY)
{
continue;
}
// no fire on unseen opponents with throwing knives
if ((Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE) &&
bPersonalKnowledge != SEEN_CURRENTLY &&
//!SoldierToSoldierLineOfSightTest(pSoldier, pOpponent, TRUE, CALC_FROM_ALL_DIRS))
!LOS_Raised(pSoldier, pOpponent, CALC_FROM_ALL_DIRS))
{
continue;
}
// sevenfm: only enemy team can use blind suppression fire
if (fSuppression &&
pSoldier->bTeam != ENEMY_TEAM)
{
continue;
}
// sevenfm: only try to suppress alive and conscious human targets
if (fSuppression &&
(pOpponent->stats.bLife < OKLIFE ||
pOpponent->bCollapsed && pOpponent->bBreath == 0 ||
pOpponent->IsCowering() ||
pOpponent->IsZombie() ||
!IS_MERC_BODY_TYPE(pOpponent)))
{
continue;
}
// shoot through wall check
if (bPersonalKnowledge != SEEN_CURRENTLY &&
//!SoldierToSoldierLineOfSightTest(pSoldier, pOpponent, TRUE, CALC_FROM_ALL_DIRS) &&
!LOS_Raised(pSoldier, pOpponent, CALC_FROM_ALL_DIRS) &&
!SoldierToVirtualSoldierLineOfSightTest(pSoldier, sTarget, bLevel, ANIM_STAND, TRUE, NO_DISTANCE_LIMIT) &&
!LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, sTarget, bLevel, TRUE, NO_DISTANCE_LIMIT) &&
!fReturnFire &&
!(InARoom(sTarget, NULL) && bLevel == 0 && Weapon[pSoldier->usAttackingWeapon].ubWeaponType == GUN_LMG) && // bPublicKnowledge == SEEN_CURRENTLY &&
!(InARoom(sTarget, NULL) && bLevel == 0 && TeamPercentKilled(pSoldier->bTeam) > (100 - 20 * SoldierDifficultyLevel(pSoldier))))
{
continue;
}
#ifdef DEBUGATTACKS
DebugAI(String("%s sees %s at gridno %d\n", pSoldier->GetName(), ExtMen[pOpponent->ubID].GetName(), pOpponent->sGridNo));
#endif
sMinAPcost = MinAPsToAttack(pSoldier, sTarget, DONTADDTURNCOST, 0);
// later will be decide if shoot is possible this here is just best guess so ignore turnover
// if we don't have enough APs left to shoot even a snap-shot at this guy
if (sMinAPcost > pSoldier->bActionPoints)
continue; // next opponent
// sevenfm: check CTGT and friendly fire for each stance instead since they can be different
// calculate chance to get through the opponent's cover (if any)
//dnl ch61 180813
/*gUnderFire.Clear();
gUnderFire.Enable();
ubChanceToGetThrough = AISoldierToSoldierChanceToGetThrough( pSoldier, pOpponent );
ubFriendlyFireChance = gUnderFire.Chance(pSoldier->bTeam, pSoldier->bSide, TRUE);
gUnderFire.Disable();
// if we can't possibly get through all the cover
if (ubChanceToGetThrough == 0)
continue; // next opponent
// sevenfm: ignore opponent if we can hit friend
if (ubFriendlyFireChance > MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE)
continue;*/
if ((pSoldier->flags.uiStatusFlags & SOLDIER_MONSTER) && (pSoldier->ubBodyType != QUEENMONSTER))
{
STRUCTURE_FILE_REF* pStructureFileRef;
UINT16 usAnimSurface;
usAnimSurface = DetermineSoldierAnimationSurface(pSoldier, pSoldier->usUIMovementMode);
pStructureFileRef = GetAnimationStructureRef(pSoldier->ubID, usAnimSurface, pSoldier->usUIMovementMode);
if (pStructureFileRef)
{
UINT16 usStructureID;
INT8 bDir;
// must make sure that structure data can be added in the direction of the target
bDir = (INT8)GetDirectionToGridNoFromGridNo(pSoldier->sGridNo, sTarget);
// ATE: Only if we have a levelnode...
if (pSoldier->pLevelNode != NULL && pSoldier->pLevelNode->pStructureData != NULL)
{
usStructureID = pSoldier->pLevelNode->pStructureData->usStructureID;
}
else
{
usStructureID = INVALID_STRUCTURE_ID;
}
if (!OkayToAddStructureToWorld(pSoldier->sGridNo, pSoldier->pathing.bLevel, &(pStructureFileRef->pDBStructureRef[gOneCDirection[bDir]]), usStructureID))
{
// can't turn in that dir.... next opponent
continue;
}
}
}
iBestHitRate = 0; // reset best hit rate to minimum
//dnl ch69 130913 Hoping to optimize
// consider alternate holding mode and different scopes
// sevenfm: alt weapon holding scope mode is used only when ubAllowAlternativeWeaponHolding == 3
for (pSoldier->bScopeMode = (gGameExternalOptions.ubAllowAlternativeWeaponHolding == 3 ? USE_ALT_WEAPON_HOLD : USE_BEST_SCOPE);
pSoldier->bScopeMode <= (gGameExternalOptions.fScopeModes ? NUM_SCOPE_MODES - 1 : USE_BEST_SCOPE);
pSoldier->bScopeMode++)
{
if (pSoldier->bScopeMode == USE_ALT_WEAPON_HOLD)
{
//dnl ch71 180913 throwing knives cannot be used in fire from hip
if (Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE)
continue;
// sevenfm: hip firing allowed only for human bodytypes
if (!IS_MERC_BODY_TYPE(pSoldier))
continue;
}
if (pSoldier->bScopeMode == USE_ALT_WEAPON_HOLD || (pSoldier->bScopeMode >= USE_BEST_SCOPE && ObjList[pSoldier->bScopeMode] != NULL))
{
usTrueState = pSoldier->usAnimState; // because is used in CalculateRaiseGunCost, CalcAimingLevelsAvailableWithAP, CalculateTurningCost
iTrueLastTarget = pSoldier->sLastTarget; // because is used in MinAPsToShootOrStab
// --------- Standing ---------
ubStance = ANIM_STAND;
// sevenfm: take into account direction when checking stance
// sevenfm: shoot heavy guns in standing stance only when using hip fire
if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTarget), ubStance) &&
(pSoldier->bScopeMode == USE_ALT_WEAPON_HOLD || !Weapon[pSoldier->usAttackingWeapon].HeavyGun || !ItemIsTwoHanded(pSoldier->usAttackingWeapon) || !gGameExternalOptions.ubAllowAlternativeWeaponHolding))
{
sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance);
if (sStanceAPcost)
{
// Going up so first is stance change then turnover, do animation change before APs calculation
pSoldier->usAnimState = STANDING;
pSoldier->sLastTarget = NOWHERE;
}
GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0);
usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost);
usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0);
if (fAddingTurningCost && fAddingRaiseGunCost)//dnl ch71 180913
{
if (usRaiseGunCost > usTurningCost)
usTurningCost = 0;
else
usRaiseGunCost = 0;
}
sRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2);
sMinAPcost = sRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost;
if (pSoldier->bActionPoints - sMinAPcost >= 0)
{
// calc next attack's minimum shooting cost (excludes readying & turning & raise gun)
sMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - sMinAPcost);
// sevenfm: check CTGT and friendly fire chance for every stance
gUnderFire.Clear();
gUnderFire.Enable();
if (fSuppression)
ubChanceToGetThrough = SoldierToLocationChanceToGetThrough(pSoldier, sTarget, bLevel, 3, NOBODY);
else
ubChanceToGetThrough = SoldierToSoldierChanceToGetThrough(pSoldier, pOpponent);
ubFriendlyFireChance = gUnderFire.Chance(pSoldier->bTeam, pSoldier->bSide, TRUE);
gUnderFire.Disable();
// sevenfm: only use this stance if we can hit target and cannot hit friends
if (ubChanceToGetThrough > 0 && ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE)
{
for (sAimTime = 0; sAimTime <= sMaxPossibleAimTime; sAimTime++)
{
sChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, sAimTime, AIM_SHOT_TORSO, bLevel, STANDING);
sAimAPCost = CalcAPCostForAiming(pSoldier, sTarget, (INT8)sAimTime);
iHitRate = sChanceToHit * (pSoldier->bActionPoints - (sMinAPcost - sRawAPCost)) / (sRawAPCost + sAimAPCost);
// sevenfm: take into account CTGT for every stance
if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough ||
(Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE) && sChanceToHit > sBestChanceToHit)// rather take best chance for throwing knives
{
iBestHitRate = iHitRate;
sBestAimTime = sAimTime;
sBestChanceToHit = sChanceToHit;
ubBestChanceToGetThrough = ubChanceToGetThrough;
ubBestFriendlyFireChance = ubFriendlyFireChance;
bScopeMode = pSoldier->bScopeMode;
sBestAPcost = sMinAPcost;
ubBestStance = ubStance;
}
}
}
}
pSoldier->usAnimState = usTrueState;
pSoldier->sLastTarget = iTrueLastTarget;
}
// no crouched/prone if we are tank/using throwing knife/hip firing
if (pSoldier->bScopeMode == USE_ALT_WEAPON_HOLD || ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier) || (Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE))
continue;
// --------- Crouched ---------
ubStance = ANIM_CROUCH;
// sevenfm: take into account direction
if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTarget), ubStance))
{
// change stance then turn
sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance);
if (sStanceAPcost)
{
pSoldier->usAnimState = CROUCHING;
pSoldier->sLastTarget = NOWHERE;
}
GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0);
usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost);
usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0);
if (fAddingTurningCost && fAddingRaiseGunCost)//dnl ch71 180913
{
if (usRaiseGunCost > usTurningCost)
usTurningCost = 0;
else
usRaiseGunCost = 0;
}
sRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2);
sMinAPcost = sRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost;
if (pSoldier->bActionPoints - sMinAPcost >= 0)
{
sMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - sMinAPcost);
// sevenfm: check CTGT and friendly fire chance for every stance
gUnderFire.Clear();
gUnderFire.Enable();
if (fSuppression)
ubChanceToGetThrough = SoldierToLocationChanceToGetThrough(pSoldier, sTarget, bLevel, 3, NOBODY);
else
ubChanceToGetThrough = SoldierToSoldierChanceToGetThrough(pSoldier, pOpponent);
ubFriendlyFireChance = gUnderFire.Chance(pSoldier->bTeam, pSoldier->bSide, TRUE);
gUnderFire.Disable();
// sevenfm: only use this stance if we can hit target and cannot hit friends
if (ubChanceToGetThrough > 0 && ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE)
{
for (sAimTime = 0; sAimTime <= sMaxPossibleAimTime; sAimTime++)
{
sChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, sAimTime, AIM_SHOT_TORSO, bLevel, CROUCHING);
sAimAPCost = CalcAPCostForAiming(pSoldier, sTarget, (INT8)sAimTime);
iHitRate = sChanceToHit * (pSoldier->bActionPoints - (sMinAPcost - sRawAPCost)) / (sRawAPCost + sAimAPCost);
// sevenfm: take into account CTGT for every stance
if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough)
{
iBestHitRate = iHitRate;
sBestAimTime = sAimTime;
sBestChanceToHit = sChanceToHit;
ubBestChanceToGetThrough = ubChanceToGetThrough;
ubBestFriendlyFireChance = ubFriendlyFireChance;
bScopeMode = pSoldier->bScopeMode;
sBestAPcost = sMinAPcost;
ubBestStance = ubStance;
}
}
}
}
pSoldier->usAnimState = usTrueState;
pSoldier->sLastTarget = iTrueLastTarget;
}
// no prone stance if we have to change direction and stance at the same time
if (pSoldier->ubDirection != AIDirection(pSoldier->sGridNo, sTarget) &&
gAnimControl[pSoldier->usAnimState].ubEndHeight > ANIM_PRONE)
{
continue;
}
// --------- Prone ---------
ubStance = ANIM_PRONE;
if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTarget), ubStance))
{
sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance);
if (sStanceAPcost)
{
pSoldier->usAnimState = PRONE;
pSoldier->sLastTarget = NOWHERE;
}
GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0);
usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost);
usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0);
sRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2);
sMinAPcost = sRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost;
if (pSoldier->bActionPoints - sMinAPcost >= 0)
{
sMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - sMinAPcost);
// sevenfm: check CTGT and friendly fire chance for every stance
gUnderFire.Clear();
gUnderFire.Enable();
if (fSuppression)
ubChanceToGetThrough = SoldierToLocationChanceToGetThrough(pSoldier, sTarget, bLevel, 3, NOBODY);
else
ubChanceToGetThrough = SoldierToSoldierChanceToGetThrough(pSoldier, pOpponent);
ubFriendlyFireChance = gUnderFire.Chance(pSoldier->bTeam, pSoldier->bSide, TRUE);
gUnderFire.Disable();
// sevenfm: only use this stance if we can hit target and cannot hit friends
if (ubChanceToGetThrough > 0 && ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE)
{
for (sAimTime = 0; sAimTime <= sMaxPossibleAimTime; sAimTime++)
{
sChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, sAimTime, AIM_SHOT_TORSO, bLevel, PRONE);
sAimAPCost = CalcAPCostForAiming(pSoldier, sTarget, (INT8)sAimTime);
iHitRate = sChanceToHit * (pSoldier->bActionPoints - (sMinAPcost - sRawAPCost)) / (sRawAPCost + sAimAPCost);
// sevenfm: take into account CTGT for every stance
if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough)
{
iBestHitRate = iHitRate;
sBestAimTime = sAimTime;
sBestChanceToHit = sChanceToHit;
ubBestChanceToGetThrough = ubChanceToGetThrough;
ubBestFriendlyFireChance = ubFriendlyFireChance;
bScopeMode = pSoldier->bScopeMode;
sBestAPcost = sMinAPcost;
ubBestStance = ubStance;
}
}
}
}
pSoldier->usAnimState = usTrueState;
pSoldier->sLastTarget = iTrueLastTarget;
}
}
}
// if we can't get any kind of hit rate at all
if (iBestHitRate == 0)
continue; // next opponent
// calculate chance to REALLY hit: shoot accurately AND get past cover
ubChanceToReallyHit = (UINT8)ceil((sBestChanceToHit * ubBestChanceToGetThrough) / 100.0f);
// if we can't REALLY hit at all
if (ubChanceToReallyHit == 0)
continue; // next opponent
// really limit knife throwing so it doesn't look wrong
if (Item[pSoldier->usAttackingWeapon].usItemClass == IC_THROWING_KNIFE &&
(ubChanceToReallyHit < 25 || (PythSpacesAway(pSoldier->sGridNo, sTarget) > CalcMaxTossRange(pSoldier, pSoldier->usAttackingWeapon, FALSE))))// Madd / 2 ) ) ) //dnl ch69 160913 was ubChanceToReallyHit < 30
continue; // don't bother... next opponent
// calculate this opponent's threat value (factor in my cover from him)
iThreatValue = CalcManThreatValue(pOpponent, pSoldier->sGridNo, TRUE, pSoldier);
// estimate the damage this shot would do to this opponent
iEstDamage = EstimateShotDamage(pSoldier, pOpponent, sBestChanceToHit);
//NumMessage("SHOT EstDamage = ",iEstDamage);
// calculate the combined "attack value" for this opponent
// highest possible value before division should be about 1.8 billion...
// normal value before division should be about 5 million...
iAttackValue = (iEstDamage * iBestHitRate * ubChanceToReallyHit * iThreatValue) / 1000;
//NumMessage("SHOT AttackValue = ",iAttackValue / 1000);
// sevenfm: penalize suppression fire
if (fSuppression)
{
// 25% penalty for shooting at invisible target
iAttackValue = iAttackValue / 2;
}
// special stuff for assassins to ignore militia more
if (pSoldier->IsAssassin() && pOpponent->bTeam == MILITIA_TEAM)
{
iAttackValue /= 2;
}
// sevenfm: empty vehicles have very low priority
if (pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle(pOpponent->bVehicleID) == 0)
{
iAttackValue /= 4;
}
// sevenfm: dying, cowering or unconscious soldiers have very low priority
if (pOpponent->stats.bLife < OKLIFE || pOpponent->bCollapsed || pOpponent->bBreathCollapsed)
{
iAttackValue /= 4;
}
#ifdef DEBUGATTACKS
DebugAI(String("CalcBestShot: best AttackValue vs %d = %d\n", uiLoop, iAttackValue));
#endif
// if we can hurt the guy, OR probably not, but at least it's our best
// chance to actually hit him and maybe scare him, knock him down, etc.
if ((iAttackValue > 0) || (ubChanceToReallyHit > pBestShot.ubChanceToReallyHit))
{
#if 0 // if there already was another viable target
if (pBestShot.ubChanceToReallyHit > 0)
{
// OK, how does our chance to hit him compare to the previous best one?
iPercentBetter = ((ubChanceToReallyHit * 100) / pBestShot.ubChanceToReallyHit) - 100;
//dnl ch62 180813 ignore firing into breathless targets if there are targets in better condition
// sevenfm: check that best opponent exists
if (pBestShot.ubOpponent != NOBODY &&
(Menptr[pBestShot.ubOpponent].bCollapsed || Menptr[pBestShot.ubOpponent].bBreathCollapsed) &&
Menptr[pBestShot.ubOpponent].bBreath < OKBREATH
&& Menptr[pBestShot.ubOpponent].bBreath < pOpponent->bBreath)
{
iPercentBetter = PERCENT_TO_IGNORE_THREAT;
}
// sevenfm: if best opponent is dying and new opponent is ok, use new opponent
if (pBestShot.ubOpponent != NOBODY &&
Menptr[pBestShot.ubOpponent].stats.bLife < OKLIFE &&
pOpponent->stats.bLife >= OKLIFE)
{
iPercentBetter = PERCENT_TO_IGNORE_THREAT;
}
// if this chance to really hit is more than 50% worse, and the other
// guy is conscious at all
if (iPercentBetter < -PERCENT_TO_IGNORE_THREAT &&
pBestShot.ubOpponent != NOBODY &&
Menptr[pBestShot.ubOpponent].stats.bLife >= OKLIFE)
{
// then stick with the older guy as the better target
continue;
}
// if this chance to really hit between 50% worse to 50% better
if (iPercentBetter < PERCENT_TO_IGNORE_THREAT)
{
// then the one with the higher ATTACK VALUE is the better target
if (iAttackValue < pBestShot.iAttackValue)
// the previous guy is more important since he's more dangerous
continue; // next opponent
}
}
// sevenfm: if new opponent is dying and best opponent is ok, ignore new opponent
if (pBestShot.ubOpponent != NOBODY &&
Menptr[pBestShot.ubOpponent].stats.bLife >= OKLIFE &&
pOpponent->stats.bLife < OKLIFE)
{
//DebugShot(pSoldier, String("new opponent is dying, best opponent is ok - skip"));
continue;
}
#endif
// OOOF! That was a lot of work! But we've got a new viable target!
pBestShot.ubPossible = TRUE;
pBestShot.ubOpponent = pOpponent->ubID;
pBestShot.ubAimTime = sBestAimTime;
pBestShot.ubChanceToReallyHit = ubChanceToReallyHit;
pBestShot.sTarget = sTarget;
pBestShot.bTargetLevel = bLevel;
pBestShot.iAttackValue = iAttackValue;
pBestShot.ubAPCost = sBestAPcost;
pBestShot.ubStance = ubBestStance;
pBestShot.bScopeMode = bScopeMode;
pBestShot.ubFriendlyFireChance = ubBestFriendlyFireChance;
possibleShots.push_back(pBestShot);
}
}
pSoldier->bScopeMode = USE_BEST_SCOPE; // better reset this back
}
bool CompareAttacks(const ATTACKTYPE& a, const ATTACKTYPE& b)
{
if (a.iAttackValue > b.iAttackValue)
return true;
else if (a.iAttackValue < b.iAttackValue)
return false;
if (a.ubChanceToReallyHit > b.ubChanceToReallyHit)
return true;
else if (a.ubChanceToReallyHit < b.ubChanceToReallyHit)
return false;
if (a.ubFriendlyFireChance < b.ubFriendlyFireChance)
return true;
else if (a.ubFriendlyFireChance > b.ubFriendlyFireChance)
return false;
if (a.ubAPCost < b.ubAPCost)
return true;
else if (a.ubAPCost > b.ubAPCost)
return false;
return false;
}
void CheckIfShotsPossible(SOLDIERTYPE* pSoldier, std::vector<ATTACKTYPE>& possibleShots)
{
INT8 guns[] = { FindAIUsableObjClass(pSoldier, IC_GUN), FindAIUsableObjClass(pSoldier, IC_GUN, TRUE) };
// Only merc bodytypes can use a sidearm, also don't bother to check for targets if found sidearm is the same as main gun
if (!IS_MERC_BODY_TYPE(pSoldier) || guns[0] == guns[1])
{
guns[1] = NO_SLOT;
}
for (size_t i = 0; i < 2; i++)
{
const auto gun = guns[i];
// if the soldier does have a gun
if (gun != NO_SLOT)
{
// if it's in his holster, swap it into his hand temporarily
if (gun != HANDPOS)
{
RearrangePocket(pSoldier, HANDPOS, gun, TEMPORARILY);
}
// get the minimum cost to attack with this item
INT16 ubMinAPcost = MinAPsToAttack(pSoldier, pSoldier->sLastTarget, ADDTURNCOST, 0);
// if we can afford the minimum AP cost
if (pSoldier->bActionPoints >= ubMinAPcost)
{
// then look around for worthy targets
CalculatePossibleShots(pSoldier, gun, possibleShots);
}
// if it was in his holster, swap it back into his holster for now
if (gun != HANDPOS)
{
RearrangePocket(pSoldier, HANDPOS, gun, TEMPORARILY);
}
}
}
}