From 5903abee093a34eff560d363fab0abe8d9c9cbe1 Mon Sep 17 00:00:00 2001 From: Sevenfm Date: Fri, 7 Aug 2020 12:12:31 +0000 Subject: [PATCH] Various AI improvements. FindThrowableGrenade: allows to specify type and damage of searched grenade. New counter SOLDIER_COUNTER_RETREAT allows to set number of turns which soldier will try to retreat and take cover. FindBestNearbyCover: use PrepareThreatlist as unified function to make list of all known opponents. Militia Control: - start retreating mode for 2 turns when ordering retreat - replace AI_ACTION_RUN_AWAY with AI_ACTION_TAKE_COVER order so that militia do not retreat from sector New AI behavior: - soldier can use smoke grenade to cover himself when under fire - soldier can use use smoke grenade on his friend to cover him - if soldier used smoke, he starts retreat mode for 2 turns - added retreat code to Red, Black AI - improved Main Red AI for civilians (they only run away) Improved suppression code: - less chance of suppression when opponent has high shock level - prefer using suppression fire when soldier is in cover git-svn-id: https://ja2svn.mooo.com/source/ja2/trunk/GameSource/ja2_v1.13/Build@8879 3b4a5df2-a311-0410-b5c6-a8a6f20db521 --- Tactical/Items.cpp | 94 +++++-- Tactical/Items.h | 20 +- Tactical/Militia Control.cpp | 21 +- Tactical/Soldier Control.cpp | 39 +++ Tactical/Soldier Control.h | 12 +- Tactical/Weapons.cpp | 2 +- Tactical/Weapons.h | 16 -- TacticalAI/AIInternals.h | 24 +- TacticalAI/AIMain.cpp | 2 +- TacticalAI/AIUtils.cpp | 300 +++++++++++++++++++-- TacticalAI/Attacks.cpp | 497 +++++++++++++++++++++++++++++++++- TacticalAI/DecideAction.cpp | 509 +++++++++++++++++++++++++++-------- TacticalAI/FindLocations.cpp | 107 +------- TacticalAI/ai.h | 8 +- 14 files changed, 1359 insertions(+), 292 deletions(-) diff --git a/Tactical/Items.cpp b/Tactical/Items.cpp index 76a3feb42..94bbf342a 100644 --- a/Tactical/Items.cpp +++ b/Tactical/Items.cpp @@ -1870,48 +1870,106 @@ INT8 FindGLGrenade( SOLDIERTYPE * pSoldier ) return( NO_SLOT ); } -INT8 FindThrowableGrenade( SOLDIERTYPE * pSoldier ) +INT8 FindThrowableGrenade(SOLDIERTYPE * pSoldier, UINT8 ubGrenadeType, UINT8 ubMinDamage) { INT8 bLoop; BOOLEAN fCheckForFlares = FALSE; + UINT16 usItem; + INT8 invsize = (INT8)pSoldier->inv.size(); + UINT8 ubDamage; // JA2Gold: give some priority to looking for flares when at night // this is AI only so we can put in some customization for night - if (GetTimeOfDayAmbientLightLevel() == NORMAL_LIGHTLEVEL_NIGHT) + // sevenfm: use flare only if cannot see opponent + if (NightLight() && + pSoldier->aiData.bOppCnt == 0 && + ubGrenadeType == EXPLOSV_ANY_TYPE && + Chance(50)) { - if (pSoldier->stats.bLife > (pSoldier->stats.bLifeMax / 2)) - { - fCheckForFlares = TRUE; - } + fCheckForFlares = TRUE; } - INT8 invsize = (INT8)pSoldier->inv.size(); + INT8 bFoundSlot = NO_SLOT; + UINT8 ubType; + UINT8 ubFoundType; - if (fCheckForFlares) + for (bLoop = 0; bLoop < invsize; ++bLoop) { - // Do a priority check for flares first - for (bLoop = 0; bLoop < invsize; ++bLoop) + if (pSoldier->inv[bLoop].exists() == true) { - if (pSoldier->inv[bLoop].exists() == true) { - if ( Item[pSoldier->inv[ bLoop ].usItem].flare ) + usItem = pSoldier->inv[bLoop].usItem; + ubType = (UINT8)Explosive[Item[usItem].ubClassIndex].ubType; + ubDamage = Explosive[Item[usItem].ubClassIndex].ubDamage; + + if ((Item[usItem].usItemClass & IC_GRENADE) && + Item[usItem].ubCursor == TOSSCURS && + GetLauncherFromLaunchable(usItem) == NOTHING && + (!Item[usItem].flare || NightLight()) && + ubDamage >= ubMinDamage && + (ubGrenadeType == EXPLOSV_ANY_TYPE || ubType == ubGrenadeType)) + { + // return if flare grenade has priority + if (fCheckForFlares && Item[usItem].flare) { - return( bLoop ); + return bLoop; + } + + // return if found needed type + if (ubType == ubGrenadeType) + { + return bLoop; + } + + // always prefer normal type + // smoke grenades have low priority + // mustard gas has priority over tear gas + if (bFoundSlot == NO_SLOT || + ubType == EXPLOSV_NORMAL && ubFoundType != EXPLOSV_NORMAL || + ubType != EXPLOSV_SMOKE && ubFoundType == EXPLOSV_SMOKE || + ubType == EXPLOSV_MUSTGAS && ubFoundType == EXPLOSV_TEARGAS) + { + bFoundSlot = bLoop; + ubFoundType = ubType; } } } } + return(bFoundSlot); +} + +UINT8 CountThrowableGrenades(SOLDIERTYPE * pSoldier, UINT8 ubGrenadeType, UINT8 ubMinDamage) +{ + INT8 bLoop; + UINT16 usItem; + INT8 invsize = (INT8)pSoldier->inv.size(); + UINT8 ubDamage; + + INT8 bFoundSlot = NO_SLOT; + UINT8 ubType; + //UINT8 ubFoundType; + UINT8 ubCnt = 0; + for (bLoop = 0; bLoop < invsize; ++bLoop) { - if (pSoldier->inv[bLoop].exists() == true) { - if ( (Item[ pSoldier->inv[ bLoop ].usItem ].usItemClass & IC_GRENADE) && // Try this check instead, to avoid tossing RPG rounds !GLGrenadeInSlot( pSoldier, bLoop ) && - GetLauncherFromLaunchable( pSoldier->inv[ bLoop ].usItem) == NOTHING ) + if (pSoldier->inv[bLoop].exists() == true) + { + usItem = pSoldier->inv[bLoop].usItem; + ubType = (UINT8)Explosive[Item[usItem].ubClassIndex].ubType; + ubDamage = Explosive[Item[usItem].ubClassIndex].ubDamage; + + if ((Item[usItem].usItemClass & IC_GRENADE) && + Item[usItem].ubCursor == TOSSCURS && + GetLauncherFromLaunchable(usItem) == NOTHING && + ubDamage >= ubMinDamage && + (ubGrenadeType == EXPLOSV_ANY_TYPE || ubType == ubGrenadeType)) { - return( bLoop ); + ubCnt++; } } } - return( NO_SLOT ); + + return ubCnt; } INT16 FindAttachmentSlot( OBJECTTYPE* pObj, UINT16 usItem, UINT8 subObject) diff --git a/Tactical/Items.h b/Tactical/Items.h index fe93e9249..b1cf30c15 100644 --- a/Tactical/Items.h +++ b/Tactical/Items.h @@ -16,6 +16,23 @@ class SOLDIERTYPE; #define NIGHTSIGHTGOGGLES_BONUS 2 * STRAIGHT_RATIO #define UVGOGGLES_BONUS 4 * STRAIGHT_RATIO +enum +{ + EXPLOSV_NORMAL, + EXPLOSV_STUN, + EXPLOSV_TEARGAS, + EXPLOSV_MUSTGAS, + EXPLOSV_FLARE, + EXPLOSV_NOISE, + EXPLOSV_SMOKE, + EXPLOSV_CREATUREGAS, + EXPLOSV_BURNABLEGAS, + EXPLOSV_FLASHBANG, + EXPLOSV_SIGNAL_SMOKE, + EXPLOSV_SMOKE_DEBRIS, + EXPLOSV_ANY_TYPE, +}; + extern UINT8 SlotToPocket[7]; extern BOOLEAN WeaponInHand( SOLDIERTYPE * pSoldier ); @@ -31,7 +48,8 @@ extern INT8 FindEmptySlotWithin(SOLDIERTYPE * pSoldier, INT8 bLower, INT8 bUpper extern INT8 FindObjInObjRange(SOLDIERTYPE * pSoldier, UINT16 usItem1, UINT16 usItem2); extern INT8 FindLaunchable(SOLDIERTYPE * pSoldier, UINT16 usWeapon); extern INT8 FindGLGrenade(SOLDIERTYPE * pSoldier); -extern INT8 FindThrowableGrenade(SOLDIERTYPE * pSoldier); +extern INT8 FindThrowableGrenade(SOLDIERTYPE * pSoldier, UINT8 ubGrenadeType = EXPLOSV_ANY_TYPE, UINT8 ubMinDamaga = 0); +extern UINT8 CountThrowableGrenades(SOLDIERTYPE * pSoldier, UINT8 ubGrenadeType = EXPLOSV_ANY_TYPE, UINT8 ubMinDamaga = 0); extern INT8 FindUsableObj(SOLDIERTYPE * pSoldier, UINT16 usItem); void DeleteObj(OBJECTTYPE * pObj ); diff --git a/Tactical/Militia Control.cpp b/Tactical/Militia Control.cpp index b0270ccb2..7798999e6 100644 --- a/Tactical/Militia Control.cpp +++ b/Tactical/Militia Control.cpp @@ -1378,6 +1378,8 @@ void MilitiaControlMenuBtnCallBack( MOUSE_REGION * pRegion, INT32 iReason ) pTMilitiaSoldier->aiData.bAttitude = DEFENSIVE; pTMilitiaSoldier->usUIMovementMode = RUNNING; + pTMilitiaSoldier->RetreatCounterStart(2); + // set up next action to run away sActionGridNo = FindSpotMaxDistFromOpponents( pTMilitiaSoldier ); @@ -1385,7 +1387,10 @@ void MilitiaControlMenuBtnCallBack( MOUSE_REGION * pRegion, INT32 iReason ) if (!TileIsOutOfBounds(pTMilitiaSoldier->aiData.usNextActionData)) { - pTMilitiaSoldier->aiData.bNextAction = AI_ACTION_RUN_AWAY; + // sevenfm: use TAKE_COVER order to stop soldier from retreating + pTMilitiaSoldier->aiData.bNextAction = AI_ACTION_TAKE_COVER; + //pTMilitiaSoldier->aiData.bNextAction = AI_ACTION_RUN_AWAY; + pTMilitiaSoldier->aiData.usActionData = ANIM_STAND; // SEND PENDING ACTION @@ -1635,14 +1640,7 @@ void MilitiaControlMenuBtnCallBack( MOUSE_REGION * pRegion, INT32 iReason ) pTeamSoldier->aiData.bAttitude = DEFENSIVE; pTeamSoldier->usUIMovementMode = RUNNING; - //// set up next action to run away - //pTeamSoldier->usNextActionData = FindSpotMaxDistFromOpponents( pTeamSoldier ); - - //if ( !TileIsOutOfBounds(pTeamSoldier->usNextActionData) ) - //{ - // pTeamSoldier->bNextAction = AI_ACTION_RUN_AWAY; - // pTeamSoldier->usActionData = ANIM_STAND; - //} + pTeamSoldier->RetreatCounterStart(2); // set up next action to run away sActionGridNo = FindSpotMaxDistFromOpponents( pTeamSoldier ); @@ -1651,7 +1649,10 @@ void MilitiaControlMenuBtnCallBack( MOUSE_REGION * pRegion, INT32 iReason ) if ( !TileIsOutOfBounds(pTeamSoldier->aiData.usNextActionData) ) { - pTeamSoldier->aiData.bNextAction = AI_ACTION_RUN_AWAY; + // sevenfm: use TAKE_COVER order to stop soldier from retreating + //pTeamSoldier->aiData.bNextAction = AI_ACTION_RUN_AWAY; + pTeamSoldier->aiData.bNextAction = AI_ACTION_TAKE_COVER; + pTeamSoldier->aiData.usActionData = ANIM_STAND; // SEND PENDING ACTION diff --git a/Tactical/Soldier Control.cpp b/Tactical/Soldier Control.cpp index 211703daa..b0c67ff34 100644 --- a/Tactical/Soldier Control.cpp +++ b/Tactical/Soldier Control.cpp @@ -25864,3 +25864,42 @@ BOOLEAN SOLDIERTYPE::IsFlanking(void) return TRUE; } + +UINT8 SOLDIERTYPE::ShockLevelPercent(void) +{ + if (gGameExternalOptions.ubMaxSuppressionShock == 0) + return 0; + + return min(100, 100 * this->aiData.bShock / gGameExternalOptions.ubMaxSuppressionShock); +} + +BOOLEAN SOLDIERTYPE::TakenLargeHit(void) +{ + if (this->usSoldierFlagMask2 & SOLDIER_TAKEN_LARGE_HIT) + return TRUE; + + return FALSE; +} + +BOOLEAN SOLDIERTYPE::IsCowering(void) +{ + if (this->usAnimState == COWERING || this->usAnimState == COWERING_PRONE) + return TRUE; + + return FALSE; +} + +void SOLDIERTYPE::RetreatCounterStart(UINT16 usValue) +{ + usSkillCounter[SOLDIER_COUNTER_RETREAT] = max(usValue, usSkillCounter[SOLDIER_COUNTER_RETREAT]); +} + +void SOLDIERTYPE::RetreatCounterStop(void) +{ + usSkillCounter[SOLDIER_COUNTER_RETREAT] = 0; +} + +UINT16 SOLDIERTYPE::RetreatCounterValue(void) +{ + return usSkillCounter[SOLDIER_COUNTER_RETREAT]; +} diff --git a/Tactical/Soldier Control.h b/Tactical/Soldier Control.h index 2aa78769b..749554ea9 100644 --- a/Tactical/Soldier Control.h +++ b/Tactical/Soldier Control.h @@ -631,6 +631,7 @@ enum { SOLDIER_COUNTER_RADIO_ARTILLERY, // there is actually no need for an artillery timer, but we use one to forbid the same AI guy ordering multiple strikes at once SOLDIER_COUNTER_SPOTTER, // used to determine wether we are a spotter SOLDIER_COUNTER_ROLE_OBSERVED, // every turn that the player observes an enemy, the enemies counter is increased. If it is high enough, we know his role + SOLDIER_COUNTER_RETREAT, // use to retreat from position SOLDIER_COUNTER_MAX = 20, // enough space for fillers }; @@ -1862,6 +1863,14 @@ public: // sevenfm: service functions BOOLEAN IsFlanking(void); + BOOLEAN CheckInitialAP(void); + UINT8 ShockLevelPercent(void); + BOOLEAN TakenLargeHit(void); + BOOLEAN IsCowering(void); + + void RetreatCounterStart(UINT16 usValue); + void RetreatCounterStop(void); + UINT16 RetreatCounterValue(void); // Flugente: prisoner system BOOLEAN CanProcessPrisoners(); @@ -1875,9 +1884,6 @@ public: // Flugente: are we an assassin? BOOLEAN IsAssassin(); - // sevenfm: service functions - BOOLEAN CheckInitialAP(void); - // Flugente: multi-turn actions UINT8 GetMultiTurnAction(); void StartMultiTurnAction( UINT8 usActionType, INT32 asGridNo ); diff --git a/Tactical/Weapons.cpp b/Tactical/Weapons.cpp index 64f74861a..46da42210 100644 --- a/Tactical/Weapons.cpp +++ b/Tactical/Weapons.cpp @@ -4734,7 +4734,7 @@ BOOLEAN UseHandToHand( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, BOOLEAN fStea UINT8 ubCounterattackChance = EffectiveDexterity(pTargetSoldier, FALSE) * (100 + pTargetSoldier->bBreath) / 200; // halve chance for counterattack if boxer was hit recently - if (pTargetSoldier->usSoldierFlagMask2 & SOLDIER_TAKEN_LARGE_HIT) + if (pTargetSoldier->TakenLargeHit()) ubCounterattackChance /= 2; // sevenfm: possibly counterattack when boxing diff --git a/Tactical/Weapons.h b/Tactical/Weapons.h index 67e04d344..d6d4e54e6 100644 --- a/Tactical/Weapons.h +++ b/Tactical/Weapons.h @@ -265,22 +265,6 @@ typedef struct CHAR8 shotAnimation[MAX_ANIMATION_FILENAME_LEN]; } AMMOTYPE; -enum -{ - EXPLOSV_NORMAL, - EXPLOSV_STUN, - EXPLOSV_TEARGAS, - EXPLOSV_MUSTGAS, - EXPLOSV_FLARE, - EXPLOSV_NOISE, - EXPLOSV_SMOKE, - EXPLOSV_CREATUREGAS, - EXPLOSV_BURNABLEGAS, - EXPLOSV_FLASHBANG, - EXPLOSV_SIGNAL_SMOKE, - EXPLOSV_SMOKE_DEBRIS, -}; - #define AMMO_DAMAGE_ADJUSTMENT_BUCKSHOT( x ) (x / 4) #define NUM_BUCKSHOT_PELLETS 9 diff --git a/TacticalAI/AIInternals.h b/TacticalAI/AIInternals.h index 2ab168f64..cc7afa94f 100644 --- a/TacticalAI/AIInternals.h +++ b/TacticalAI/AIInternals.h @@ -109,12 +109,16 @@ enum typedef struct { - SOLDIERTYPE * pOpponent; - INT32 sGridNo; - INT32 iValue; - INT32 iAPs; - INT32 iCertainty; - INT32 iOrigRange; + SOLDIERTYPE *pOpponent; + INT32 sGridNo; + INT32 iValue; + INT32 iAPs; + INT32 iCertainty; + INT32 iOrigRange; + INT8 bLevel; + INT8 bKnowledge; + INT8 bPersonalKnowledge; + INT8 bPublicKnowledge; } THREATTYPE; // define for bAimTime for bursting @@ -170,6 +174,7 @@ extern THREATTYPE Threat[MAXMERCS]; extern int ThreatPercent[10]; extern UINT8 SkipCoverCheck; extern INT8 GameOption[MAXGAMEOPTIONS]; +extern UINT32 guiThreatCnt; typedef enum { @@ -205,6 +210,13 @@ void CreatureCall( SOLDIERTYPE * pCaller ); INT8 CreatureDecideAction( SOLDIERTYPE * pCreature ); void CreatureDecideAlertStatus( SOLDIERTYPE *pCreature ); +// sevenfm: +void CheckTossSelfSmoke(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow); +void CheckTossFriendSmoke(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow); +void CheckTossAt(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow, INT32 sTargetSpot, INT8 bTargetLevel, UINT8 ubOpponentID); +INT32 FindTossSpotInDirection(INT32 sSpot, INT8 bLevel, INT32 sTargetSpot, BOOLEAN fCheckAdjacentDirections, BOOLEAN fCheckFarther); +void CheckTossGrenadeAt(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow, INT32 sTargetSpot, INT8 bTargetLevel, UINT8 ubGrenadeType); + // Flugente: AI INT8 ZombieDecideAction( SOLDIERTYPE* pSoldier ); INT8 ArmedVehicleDecideAction( SOLDIERTYPE* pSoldier ); diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 0b4cf85f7..6bc9e1457 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -1374,7 +1374,7 @@ void ActionDone(SOLDIERTYPE *pSoldier) UINT8 SkipCoverCheck = FALSE; THREATTYPE Threat[MAXMERCS]; - +UINT32 guiThreatCnt = 0; // threat percentage is based on the certainty of opponent knowledge: // opplist value: -4 -3 -2 -1 SEEN 1 2 3 4 5 diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index d4a70911a..36c9bbc84 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -3116,6 +3116,12 @@ INT32 RangeChangeDesire( SOLDIERTYPE * pSoldier ) iRangeFactorMultiplier = pSoldier->aiData.bAIMorale - 1; + // sevenfm: retreat + if (pSoldier->RetreatCounterValue() > 0) + { + return 0; + } + // civilians only run away if (pSoldier->aiData.bNeutral) { @@ -3673,8 +3679,8 @@ INT8 CalcMoraleNew(SOLDIERTYPE *pSoldier) return MORALE_FEARLESS; } - // sevenfm: retreat when blinded - if (pSoldier->bBlindedCounter > 0) + // sevenfm: retreat, also when blinded + if (pSoldier->RetreatCounterValue() > 0 || pSoldier->bBlindedCounter > 0) { return MORALE_WORRIED; } @@ -3950,7 +3956,7 @@ UINT8 CountNearbyFriendsLastAttackHit( SOLDIERTYPE *pSoldier, INT32 sGridNo, UIN return ubFriendCount; } -UINT8 CountFriendsFlankSameSpot( SOLDIERTYPE *pSoldier ) +UINT8 CountFriendsFlankSameSpot(SOLDIERTYPE *pSoldier, INT32 sSpot) { CHECKF(pSoldier); @@ -3960,31 +3966,34 @@ UINT8 CountFriendsFlankSameSpot( SOLDIERTYPE *pSoldier ) UINT8 ubFlankLeft = 0; UINT8 ubFlankRight = 0; - UINT8 ubMaxDist = VISION_RANGE / 2; - INT32 sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); + if (TileIsOutOfBounds(sSpot)) + { + sSpot = ClosestKnownOpponent(pSoldier, NULL, NULL); + } - if(TileIsOutOfBounds(sClosestOpponent)) + if (TileIsOutOfBounds(sSpot)) { return 0; } // Run through each friendly. - for ( UINT8 iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; iCounter ++ ) + for (UINT8 iCounter = gTacticalStatus.Team[pSoldier->bTeam].bFirstID; iCounter <= gTacticalStatus.Team[pSoldier->bTeam].bLastID; iCounter++) { - pFriend = MercPtrs[ iCounter ]; + pFriend = MercPtrs[iCounter]; - if (pFriend != pSoldier && - pFriend->bActive && + if (pFriend && + pFriend != pSoldier && + pFriend->bActive && pFriend->stats.bLife >= OKLIFE && pFriend->aiData.bAlertStatus == STATUS_RED && pFriend->aiData.bOrders > ONGUARD && - pFriend->aiData.bOrders != SNIPER ) + 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, sClosestOpponent) < ubMaxDist ) + if (pFriend->numFlanks > 0 && pFriend->numFlanks < MAX_FLANKS_RED && + PythSpacesAway(pFriend->lastFlankSpot, sSpot) < VISION_RANGE / 2) { - if( pFriend->flags.lastFlankLeft ) + if (pFriend->flags.lastFlankLeft) { ubFlankLeft++; } @@ -5867,4 +5876,265 @@ INT32 RandomizeOpponentLocation(INT32 sSpot, SOLDIERTYPE *pOpponent, INT16 sMaxD } return sSpot; -} \ No newline at end of file +} + +// first call PrepareThreatlist to make threat list +UINT8 ClosestKnownThreatID(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + UINT32 uiLoop; + INT32 sClosestOpponent = NOWHERE; + INT32 iRange, iClosestRange; + UINT8 ubClosestOpponentID = NOBODY; + + // use global defined threat list + for (uiLoop = 0; uiLoop < guiThreatCnt; 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 +UINT8 ClosestSeenThreatID(SOLDIERTYPE *pSoldier, UINT8 ubMax) +{ + CHECKF(pSoldier); + + UINT32 uiLoop; + INT32 sClosestOpponent = NOWHERE; + INT32 iRange, iClosestRange; + UINT8 ubClosestOpponentID = NOBODY; + + // use global defined threat list + for (uiLoop = 0; uiLoop < guiThreatCnt; 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); +} + +void 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; + + guiThreatCnt = 0; + + if (!pSoldier) + return; + + // 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[guiThreatCnt].iValue = CalcManThreatValue(pOpponent, pSoldier->sGridNo, FALSE, pSoldier); + + // if the opponent is no threat at all for some reason + if (Threat[guiThreatCnt].iValue == -999) + { + continue; // check next opponent + } + + Threat[guiThreatCnt].pOpponent = pOpponent; + Threat[guiThreatCnt].sGridNo = sThreatLoc; + Threat[guiThreatCnt].iCertainty = iThreatCertainty; + Threat[guiThreatCnt].iOrigRange = iThreatRange; + + // calculate how many APs he will have at the start of the next turn + Threat[guiThreatCnt].iAPs = pOpponent->CalcActionPoints(); + + // sevenfm: more information + Threat[guiThreatCnt].bLevel = bThreatLevel; + Threat[guiThreatCnt].bKnowledge = bKnowledge; + Threat[guiThreatCnt].bPersonalKnowledge = bPersonalKnowledge; + Threat[guiThreatCnt].bPublicKnowledge = bPublicKnowledge; + + if (iThreatRange < iClosestThreatRange) + { + iClosestThreatRange = iThreatRange; + } + + guiThreatCnt++; + } +} + +UINT8 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT16 sDistance) +{ + CHECKF(pSoldier); + + UINT32 uiLoop; + SOLDIERTYPE *pOpponent; + + INT32 sThreatLoc; + INT8 iThreatLevel; + + UINT8 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; +} + +UINT8 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + UINT32 uiLoop; + SOLDIERTYPE *pOpponent; + + INT32 sThreatLoc; + INT8 iThreatLevel; + + UINT8 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; +} diff --git a/TacticalAI/Attacks.cpp b/TacticalAI/Attacks.cpp index a60980a68..17cf8b77a 100644 --- a/TacticalAI/Attacks.cpp +++ b/TacticalAI/Attacks.cpp @@ -325,8 +325,7 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) if (fSuppression && (pOpponent->stats.bLife < OKLIFE || pOpponent->bCollapsed && pOpponent->bBreath == 0 || - //pOpponent->usAnimState == COWERING || - //pOpponent->usAnimState == COWERING_PRONE || + pOpponent->IsCowering() || CoweringShockLevel(pOpponent) || pOpponent->IsZombie() || !IS_MERC_BODY_TYPE(pOpponent))) @@ -3698,4 +3697,496 @@ BOOLEAN MoreFriendsThanEnemiesinNearbysectors(UINT8 ausTeam, INT16 aX, INT16 aY, return (enemyteam > militiateam); return (militiateam > enemyteam); -} \ No newline at end of file +} + +// sevenfm: new attack functions +void CheckTossSelfSmoke(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow) +{ + INT16 ubMinAPcost; + INT8 bGrenadeIn = NO_SLOT; + + // initialize + pBestThrow->ubPossible = FALSE; + pBestThrow->ubChanceToReallyHit = 0; + pBestThrow->iAttackValue = 0; + + if (!IS_MERC_BODY_TYPE(pSoldier)) + { + return; + } + + pSoldier->bWeaponMode = WM_NORMAL; + + bGrenadeIn = FindThrowableGrenade(pSoldier, EXPLOSV_SMOKE); + + // prepare threat list for ClosestSeenThreatID(), ClosestKnownThreatID() + PrepareThreatlist(pSoldier); + + if (bGrenadeIn != NO_SLOT) + { + pBestThrow->bWeaponIn = bGrenadeIn; + + // if it's in his holster, swap it into his hand temporarily + if (pBestThrow->bWeaponIn != HANDPOS) + { + RearrangePocket(pSoldier, HANDPOS, pBestThrow->bWeaponIn, TEMPORARILY); + } + + // get the minimum cost to attack with this tossable item + ubMinAPcost = MinAPsToAttack(pSoldier, pSoldier->sGridNo, DONTADDTURNCOST, 0); + + // if we can afford the minimum AP cost to throw this tossable item + if (pSoldier->bActionPoints >= ubMinAPcost) + { + INT32 sSpot = pSoldier->sGridNo; + INT8 bLevel = pSoldier->pathing.bLevel; + + INT32 sTargetSpot = NOWHERE; + INT8 bTargetLevel = bLevel; + + INT32 sClosestThreat; + UINT8 ubClosestThreatID; + + ubClosestThreatID = pSoldier->ubPreviousAttackerID; + + // try to find good spot for smoke + if (ubClosestThreatID != NOBODY && + MercPtrs[ubClosestThreatID] && + !TileIsOutOfBounds(MercPtrs[ubClosestThreatID]->sGridNo)) + { + sClosestThreat = MercPtrs[ubClosestThreatID]->sGridNo; + + sTargetSpot = FindTossSpotInDirection(sSpot, bLevel, sClosestThreat, TRUE, TRUE); + } + + if (TileIsOutOfBounds(sTargetSpot)) + { + ubClosestThreatID = ClosestSeenThreatID(pSoldier, SEEN_LAST_TURN); + + if (ubClosestThreatID != NOBODY && + MercPtrs[ubClosestThreatID] && + !TileIsOutOfBounds(MercPtrs[ubClosestThreatID]->sGridNo)) + { + sClosestThreat = MercPtrs[ubClosestThreatID]->sGridNo; + + sTargetSpot = FindTossSpotInDirection(sSpot, bLevel, sClosestThreat, TRUE, TRUE); + } + } + + if (TileIsOutOfBounds(sTargetSpot)) + { + ubClosestThreatID = ClosestKnownThreatID(pSoldier); + + if (ubClosestThreatID != NOBODY && + MercPtrs[ubClosestThreatID] && + !TileIsOutOfBounds(MercPtrs[ubClosestThreatID]->sGridNo)) + { + sClosestThreat = MercPtrs[ubClosestThreatID]->sGridNo; + + sTargetSpot = FindTossSpotInDirection(sSpot, bLevel, sClosestThreat, TRUE, TRUE); + } + } + + if (!TileIsOutOfBounds(sTargetSpot)) + { + CheckTossAt(pSoldier, pBestThrow, sTargetSpot, bTargetLevel, pSoldier->ubID); + } + } + + // if it was in his holster, swap it back into his holster for now + if (pBestThrow->bWeaponIn != HANDPOS) + { + RearrangePocket(pSoldier, HANDPOS, pBestThrow->bWeaponIn, TEMPORARILY); + } + } + + pSoldier->bWeaponMode = WM_NORMAL; +} + +void CheckTossFriendSmoke(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow) +{ + INT16 ubMinAPcost; + INT8 bGrenadeIn = NO_SLOT; + + // initialize + pBestThrow->ubPossible = FALSE; + pBestThrow->ubChanceToReallyHit = 0; + pBestThrow->iAttackValue = 0; + pBestThrow->ubOpponent = NOBODY; + + if (!IS_MERC_BODY_TYPE(pSoldier)) + { + return; + } + + pSoldier->bWeaponMode = WM_NORMAL; + + bGrenadeIn = FindThrowableGrenade(pSoldier, EXPLOSV_SMOKE); + + if (bGrenadeIn != NO_SLOT) + { + pBestThrow->bWeaponIn = bGrenadeIn; + + // if it's in his holster, swap it into his hand temporarily + if (pBestThrow->bWeaponIn != HANDPOS) + { + RearrangePocket(pSoldier, HANDPOS, pBestThrow->bWeaponIn, TEMPORARILY); + } + + // get the minimum cost to attack with this tossable item + ubMinAPcost = MinAPsToAttack(pSoldier, pSoldier->sGridNo, DONTADDTURNCOST, 0); + + // if we can afford the minimum AP cost to throw this tossable item + if (pSoldier->bActionPoints >= ubMinAPcost) + { + INT32 sSpot = pSoldier->sGridNo; + INT8 bLevel = pSoldier->pathing.bLevel; + + // check all friends + SOLDIERTYPE * pFriend; + INT32 sClosestFriendSpot = NOWHERE; + INT8 bClosestFriendLevel = 0; + UINT8 ubClosestFriendID = NOBODY; + + INT32 sFriendSpot; + INT8 bFriendLevel; + + INT32 sClosestOpponent; + + // check adjacent spots + UINT8 ubMovementCost; + INT32 sTempGridNo; + UINT8 ubDirection; + + // Run through each friendly. + for (UINT8 iCounter = gTacticalStatus.Team[pSoldier->bTeam].bFirstID; iCounter <= gTacticalStatus.Team[pSoldier->bTeam].bLastID; iCounter++) + { + pFriend = MercPtrs[iCounter]; + + // check that friend is alive and needs cover + if (pFriend && + pFriend != pSoldier && + pFriend->bActive && + pFriend->stats.bLife >= OKLIFE && + RangeChangeDesire(pFriend) <= 3 && + (pFriend->IsFlanking() && !TileIsOutOfBounds(pFriend->lastFlankSpot) && PythSpacesAway(pFriend->sGridNo, pFriend->lastFlankSpot) < (INT16)(MAX_VISION_RANGE) && LocationToLocationLineOfSightTest(pFriend->sGridNo, pFriend->pathing.bLevel, pFriend->lastFlankSpot, pFriend->pathing.bLevel, TRUE, NO_DISTANCE_LIMIT) || + pFriend->aiData.bUnderFire && (pFriend->IsCowering() || pFriend->TakenLargeHit() || pFriend->aiData.bUnderFire && pFriend->ShockLevelPercent() > 50 && pFriend->stats.bLife < pFriend->stats.bLifeMax * 3 / 4)) + ) + { + sFriendSpot = pFriend->sGridNo; + bFriendLevel = pFriend->pathing.bLevel; + sClosestOpponent = ClosestKnownOpponent(pFriend, NULL, NULL); + + if (PythSpacesAway(sSpot, sFriendSpot) <= DAY_VISION_RANGE && + PythSpacesAway(sSpot, sFriendSpot) > DAY_VISION_RANGE / 4 && + !InSmoke(sFriendSpot, bFriendLevel) && + (!NightLight() || InLightAtNight(sFriendSpot, bFriendLevel)) && + !Water(sFriendSpot, bFriendLevel) && + !TileIsOutOfBounds(sClosestOpponent) && + PythSpacesAway(sFriendSpot, sClosestOpponent) > DAY_VISION_RANGE / 4 && + !ProneSightCoverAtSpot(pFriend, sFriendSpot, TRUE) && + //!SightCoverAtSpot(pFriend, sFriendSpot, FALSE) && + //!AnyCoverAtSpot(pFriend, sFriendSpot) && + (TileIsOutOfBounds(sClosestFriendSpot) || PythSpacesAway(sSpot, sFriendSpot) < PythSpacesAway(sSpot, sClosestFriendSpot)) && + (pFriend->TakenLargeHit() || pFriend->ShockLevelPercent() > 50 && pFriend->stats.bLife < pFriend->stats.bLifeMax * 3 / 4)) + { + // check that we can toss grenade + CheckTossAt(pSoldier, pBestThrow, sFriendSpot, bFriendLevel, pFriend->ubID); + + if (pBestThrow->ubPossible) + { + sClosestFriendSpot = sFriendSpot; + bClosestFriendLevel = bFriendLevel; + ubClosestFriendID = pFriend->ubID; + } + else + { + // find adjacent spots + for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) + { + sTempGridNo = NewGridNo(sFriendSpot, DirectionInc(ubDirection)); + + if (sTempGridNo != sFriendSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bFriendLevel]; + + if (!TileIsOutOfBounds(sTempGridNo) && + ubMovementCost < TRAVELCOST_BLOCKED && + !Water(sTempGridNo, bFriendLevel)) + { + // check this gridno + CheckTossAt(pSoldier, pBestThrow, sTempGridNo, bFriendLevel, pFriend->ubID); + + if (pBestThrow->ubPossible) + { + sClosestFriendSpot = sTempGridNo; + bClosestFriendLevel = bFriendLevel; + ubClosestFriendID = pFriend->ubID; + + break; + } + } + } + } + } + } + } + } + + // finally, prepare data for toss + pBestThrow->ubPossible = FALSE; + pBestThrow->ubChanceToReallyHit = 0; + pBestThrow->iAttackValue = 0; + pBestThrow->ubOpponent = NOBODY; + + if (!TileIsOutOfBounds(sClosestFriendSpot)) + { + CheckTossAt(pSoldier, pBestThrow, sClosestFriendSpot, bClosestFriendLevel, ubClosestFriendID); + } + } + + // if it was in his holster, swap it back into his holster for now + if (pBestThrow->bWeaponIn != HANDPOS) + { + RearrangePocket(pSoldier, HANDPOS, pBestThrow->bWeaponIn, TEMPORARILY); + } + } + + pSoldier->bWeaponMode = WM_NORMAL; +} + +// check if we can toss grenade at spot, and prepare attack data +// grenade should be in hand +void CheckTossAt(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow, INT32 sTargetSpot, INT8 bTargetLevel, UINT8 ubOpponentID) +{ + UINT16 usInHand, usGrenade; + INT32 iTossRange; + + INT32 sEndSpot = NOWHERE; + INT8 bEndLevel = 0; + + UINT8 ubAPCost; + UINT8 ubChanceToHit; + UINT8 ubChanceToReallyHit; + UINT8 ubChanceToGetThrough; + INT32 iHitRate; + INT32 iAttackValue; + INT32 iTotalThreatValue = 100; + UINT8 ubMaxPossibleAimTime = 0; + UINT16 usTrueState = pSoldier->usAnimState; + UINT8 ubStance = gAnimControl[pSoldier->usAnimState].ubEndHeight; + + usInHand = pSoldier->inv[HANDPOS].usItem; + + // initialize + pBestThrow->ubPossible = FALSE; + pBestThrow->ubChanceToReallyHit = 0; + pBestThrow->iAttackValue = 0; + + iTossRange = CalcMaxTossRange(pSoldier, usInHand, TRUE); + usGrenade = pSoldier->inv[HANDPOS].usItem; + ubChanceToGetThrough = 100 * CalculateLaunchItemChanceToGetThrough(pSoldier, &pSoldier->inv[HANDPOS], sTargetSpot, bTargetLevel, 0, &sEndSpot, TRUE, &bEndLevel, FALSE); + ubAPCost = (UINT8)MinAPsToThrow(pSoldier, sTargetSpot, TRUE) + CalcAPCostForAiming(pSoldier, sTargetSpot, ubMaxPossibleAimTime); + ubChanceToHit = (UINT8)CalcThrownChanceToHit(pSoldier, sTargetSpot, 0, AIM_SHOT_TORSO); + ubChanceToReallyHit = (ubChanceToHit * ubChanceToGetThrough) / 100; + iHitRate = (pSoldier->bActionPoints * ubChanceToHit) / ubAPCost; + iAttackValue = (iHitRate * ubChanceToReallyHit * iTotalThreatValue) / 1000; + + // maybe try to stand up for better range + if (ubChanceToReallyHit == 0 && + gAnimControl[pSoldier->usAnimState].ubEndHeight < ANIM_STAND && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTargetSpot), ANIM_STAND) && + pSoldier->bActionPoints >= ubAPCost + GetAPsToChangeStance(pSoldier, ANIM_STAND)) + { + pSoldier->usAnimState = STANDING; + + iTossRange = CalcMaxTossRange(pSoldier, usInHand, TRUE); + + usGrenade = pSoldier->inv[HANDPOS].usItem; + ubChanceToGetThrough = 100 * CalculateLaunchItemChanceToGetThrough(pSoldier, &pSoldier->inv[HANDPOS], sTargetSpot, bTargetLevel, 0, &sEndSpot, TRUE, &bEndLevel, FALSE); + ubAPCost = (UINT8)MinAPsToThrow(pSoldier, sTargetSpot, TRUE) + CalcAPCostForAiming(pSoldier, sTargetSpot, ubMaxPossibleAimTime); + ubChanceToHit = (UINT8)CalcThrownChanceToHit(pSoldier, sTargetSpot, 0, AIM_SHOT_TORSO); + ubChanceToReallyHit = (ubChanceToHit * ubChanceToGetThrough) / 100; + iHitRate = (pSoldier->bActionPoints * ubChanceToHit) / ubAPCost; + iAttackValue = (iHitRate * ubChanceToReallyHit * iTotalThreatValue) / 1000; + pSoldier->usAnimState = usTrueState; + + ubStance = ANIM_STAND; + ubAPCost += GetAPsToChangeStance(pSoldier, ANIM_STAND); + } + + if (ubChanceToReallyHit > 0) + { + // OOOF! That was a lot of work! But we've got a new best target! + pBestThrow->ubPossible = TRUE; + pBestThrow->ubOpponent = ubOpponentID; + pBestThrow->ubAimTime = ubMaxPossibleAimTime; + pBestThrow->ubChanceToReallyHit = ubChanceToReallyHit; + pBestThrow->sTarget = sTargetSpot; + pBestThrow->iAttackValue = iAttackValue; + pBestThrow->ubAPCost = ubAPCost; + pBestThrow->bTargetLevel = bTargetLevel; + pBestThrow->ubStance = ubStance; + } +} + +INT32 FindTossSpotInDirection(INT32 sSpot, INT8 bLevel, INT32 sTargetSpot, BOOLEAN fCheckAdjacentDirections, BOOLEAN fCheckFarther) +{ + // find adjacent spot + UINT8 ubMovementCost; + INT32 sTempGridNo, sOldSpot; + UINT8 ubDirection; + + // safety check + if (TileIsOutOfBounds(sSpot) || TileIsOutOfBounds(sTargetSpot)) + { + return NOWHERE; + } + + // check direction + ubDirection = AIDirection(sSpot, sTargetSpot); + + sTempGridNo = NewGridNo(sSpot, DirectionInc(ubDirection)); + + if (sTempGridNo != sSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && + !Water(sTempGridNo, bLevel)) + { + return sTempGridNo; + } + + if (fCheckFarther) + { + sOldSpot = sTempGridNo; + sTempGridNo = NewGridNo(sOldSpot, DirectionInc(ubDirection)); + + if (sTempGridNo != sOldSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && + !Water(sTempGridNo, bLevel)) + { + return sTempGridNo; + } + } + + // check C direction + ubDirection = gOneCDirection[AIDirection(sOldSpot, sTargetSpot)]; + sTempGridNo = NewGridNo(sOldSpot, DirectionInc(ubDirection)); + + if (sTempGridNo != sOldSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && !Water(sTempGridNo, bLevel)) + { + return sTempGridNo; + } + } + + // check CC direction + ubDirection = gOneCCDirection[AIDirection(sOldSpot, sTargetSpot)]; + sTempGridNo = NewGridNo(sOldSpot, DirectionInc(ubDirection)); + + if (sTempGridNo != sOldSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && !Water(sTempGridNo, bLevel)) + { + return sTempGridNo; + } + } + } + } + + if (fCheckAdjacentDirections) + { + // check C direction + ubDirection = gOneCDirection[AIDirection(sSpot, sTargetSpot)]; + sTempGridNo = NewGridNo(sSpot, DirectionInc(ubDirection)); + + if (sTempGridNo != sSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && !Water(sTempGridNo, bLevel)) + { + return sTempGridNo; + } + } + + // check CC direction + ubDirection = gOneCCDirection[AIDirection(sSpot, sTargetSpot)]; + sTempGridNo = NewGridNo(sSpot, DirectionInc(ubDirection)); + + if (sTempGridNo != sSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && !Water(sTempGridNo, bLevel)) + { + return sTempGridNo; + } + } + } + + return NOWHERE; +} + +void CheckTossGrenadeAt(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow, INT32 sTargetSpot, INT8 bTargetLevel, UINT8 ubGrenadeType) +{ + INT16 ubMinAPcost; + INT8 bGrenadeIn = NO_SLOT; + + // initialize + pBestThrow->ubPossible = FALSE; + pBestThrow->ubChanceToReallyHit = 0; + pBestThrow->iAttackValue = 0; + + if (!IS_MERC_BODY_TYPE(pSoldier)) + { + return; + } + + if (TileIsOutOfBounds(sTargetSpot)) + { + return; + } + + pSoldier->bWeaponMode = WM_NORMAL; + + bGrenadeIn = FindThrowableGrenade(pSoldier, ubGrenadeType); + + if (bGrenadeIn != NO_SLOT) + { + pBestThrow->bWeaponIn = bGrenadeIn; + + // if it's in his holster, swap it into his hand temporarily + if (pBestThrow->bWeaponIn != HANDPOS) + { + RearrangePocket(pSoldier, HANDPOS, pBestThrow->bWeaponIn, TEMPORARILY); + } + + // get the minimum cost to attack with this tossable item + ubMinAPcost = MinAPsToAttack(pSoldier, pSoldier->sGridNo, DONTADDTURNCOST, 0); + + // if we can afford the minimum AP cost to throw this tossable item + if (pSoldier->bActionPoints >= ubMinAPcost) + { + CheckTossAt(pSoldier, pBestThrow, sTargetSpot, bTargetLevel, NOBODY); + } + + // if it was in his holster, swap it back into his holster for now + if (pBestThrow->bWeaponIn != HANDPOS) + { + RearrangePocket(pSoldier, HANDPOS, pBestThrow->bWeaponIn, TEMPORARILY); + } + } + + pSoldier->bWeaponMode = WM_NORMAL; +} diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 4871e21d2..843b94811 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2437,19 +2437,25 @@ INT8 DecideActionYellow(SOLDIERTYPE *pSoldier) INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { - INT8 bActionReturned; - INT32 iDummy; - INT32 iChance,sClosestOpponent,sClosestFriend; - INT32 sClosestDisturbance, sDistVisible, sCheckGridNo; - UINT8 ubCanMove,ubOpponentDir; - INT8 bInWater, bInDeepWater, bInGas; - INT8 bSeekPts = 0, bHelpPts = 0, bHidePts = 0, bWatchPts = 0; + INT8 bActionReturned; + INT32 iDummy; + INT32 iChance; + INT32 sClosestOpponent = NOWHERE, sClosestFriend = NOWHERE; + INT32 sClosestDisturbance = NOWHERE, sCheckGridNo; + INT32 sDistVisible; + UINT8 ubCanMove,ubOpponentDir; + INT8 bInWater, bInDeepWater, bInGas; + INT8 bSeekPts = 0, bHelpPts = 0, bHidePts = 0, bWatchPts = 0; INT8 bHighestWatchLoc; ATTACKTYPE BestThrow, BestShot; // sevenfm: BOOLEAN fProneSightCover = FALSE; BOOLEAN fDangerousSpot = FALSE; + BOOLEAN fAnyCover = FALSE; + BOOLEAN fCanBeSeen = FALSE; + INT32 sOpponentGridNo; + INT8 bOpponentLevel; #ifdef AI_TIMING_TEST UINT32 uiStartTime, uiEndTime; @@ -2480,8 +2486,22 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) return(AI_ACTION_NONE); } + // sevenfm: find closest opponent + sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); + + if (!SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE)) + { + fCanBeSeen = TRUE; + DebugAI(AI_MSG_INFO, pSoldier, String("can be seen")); + } + fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); - if( !fProneSightCover || pSoldier->aiData.bUnderFire ) + DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover)); + fAnyCover = AnyCoverAtSpot(pSoldier, pSoldier->sGridNo); + DebugAI(AI_MSG_INFO, pSoldier, String("any cover %d", fAnyCover)); + + if (!fProneSightCover || pSoldier->aiData.bUnderFire) { fDangerousSpot = TRUE; } @@ -2797,6 +2817,68 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + // use smoke to cover friend + DebugAI(AI_MSG_TOPIC, pSoldier, String("[use smoke to cover friend]")); + if (gfTurnBasedAI && + SoldierAI(pSoldier) && + !bInWater && + !bInGas && + !pSoldier->IsFlanking() && + pSoldier->CheckInitialAP() && + !pSoldier->aiData.bUnderFire && + SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE) && + !AICheckIsSniper(pSoldier) && + !AICheckIsMachinegunner(pSoldier) && + !AICheckIsMortarOperator(pSoldier) && + Chance(100 - min(100, 10 * CountPublicKnownEnemies(pSoldier, pSoldier->sGridNo, DAY_VISION_RANGE))) && + !GuySawEnemy(pSoldier, SEEN_LAST_TURN)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("check if we can cover friend with smoke")); + + CheckTossFriendSmoke(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + + // start retreating for several turns + if (BestThrow.ubOpponent != NOBODY && !MercPtrs[BestThrow.ubOpponent]->IsFlanking()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("start retreat counter for %d", BestThrow.ubOpponent)); + MercPtrs[BestThrow.ubOpponent]->RetreatCounterStart(2); + } + + // if necessary, swap the usItem from holster into the hand position + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("throw smoke grenade to cover friend %d at spot %d level %d", BestThrow.ubOpponent, BestThrow.sTarget, BestThrow.bTargetLevel)); + + return(AI_ACTION_TOSS_PROJECTILE); + } + } + // SNIPER! CheckIfShotPossible(pSoldier, &BestShot); DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("decideactionred: is sniper shot possible? = %d, CTH = %d",BestShot.ubPossible,BestShot.ubChanceToReallyHit)); @@ -2935,6 +3017,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !TileIsOutOfBounds(BestShot.sTarget) && BestShot.ubOpponent != NOBODY && MercPtrs[BestShot.ubOpponent] && + Chance(100 - MercPtrs[BestShot.ubOpponent]->ShockLevelPercent() / 2) && // check weapon/ammo requirements IsGunAutofireCapable(&pSoldier->inv[BestShot.bWeaponIn]) && GetMagSize(&pSoldier->inv[BestShot.bWeaponIn]) >= gGameExternalOptions.ubAISuppressionMinimumMagSize && @@ -2943,6 +3026,14 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bOrders != SNIPER && BestShot.ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE && !AICheckIsFlanking(pSoldier) && + !pSoldier->RetreatCounterValue() && + // check cover + (fAnyCover || // safe position + !fCanBeSeen && NightLight() && CountFriendsFlankSameSpot(pSoldier, sClosestOpponent) && Chance(50) || + ARMED_VEHICLE(pSoldier) || // tanks don't need cover + pSoldier->aiData.bUnderFire && (pSoldier->ubPreviousAttackerID == BestShot.ubOpponent || pSoldier->ubNextToPreviousAttackerID == BestShot.ubOpponent || MercPtrs[BestShot.ubOpponent]->sLastTarget == pSoldier->sGridNo) || // return fire + Chance((BestShot.ubChanceToReallyHit + 100) / 2) || // 50% chance to fire without cover + SoldierToSoldierLineOfSightTest(pSoldier, MercPtrs[BestShot.ubOpponent], TRUE, CALC_FROM_ALL_DIRS)) && // can see target after turning // reduce chance to shoot if target is beyond weapon range (AICheckIsMachinegunner(pSoldier) || ARMED_VEHICLE(pSoldier) || @@ -3277,6 +3368,34 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + //////////////////////////////////////////////////////////////////////// + // RED RETREAT + //////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[retreat]")); + if (gfTurnBasedAI && + !fCivilian && + !bInWater && + ubCanMove && + SoldierAI(pSoldier) && + pSoldier->aiData.bOrders != STATIONARY && + pSoldier->aiData.bOrders != SNIPER && + pSoldier->RetreatCounterValue() > 0 && + (pSoldier->CheckInitialAP() || !fAnyCover || pSoldier->aiData.bUnderFire)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("search for retreat spot")); + INT32 sRetreatSpot = FindRetreatSpot(pSoldier); + + if (!TileIsOutOfBounds(sRetreatSpot)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("found retreat spot %d", sRetreatSpot)); + + //BeginMultiPurposeLocator(sRetreatSpot, pSoldier->pathing.bLevel, FALSE); + + pSoldier->aiData.usActionData = sRetreatSpot; + return(AI_ACTION_TAKE_COVER); + } + } + DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: crouch and rest if running out of breath"); //////////////////////////////////////////////////////////////////////// // CROUCH & REST IF RUNNING OUT OF BREATH @@ -3429,6 +3548,59 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + DebugAI(AI_MSG_TOPIC, pSoldier, String("[self smoke when under fire]")); + if (gfTurnBasedAI && + pSoldier->bActionPoints == pSoldier->bInitialActionPoints && + pSoldier->aiData.bUnderFire && + !InARoom(pSoldier->sGridNo, NULL) && + !InSmoke(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + RangeChangeDesire(pSoldier) <= 2 && + (!NightLight() || InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel)) && + !TileIsOutOfBounds(sClosestOpponent) && + PythSpacesAway(pSoldier->sGridNo, sClosestOpponent) > DAY_VISION_RANGE / 4 && + (!fProneSightCover && !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->TakenLargeHit()) && + (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("check if soldier can cover himself with smoke")); + + CheckTossSelfSmoke(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + + // start retreating for several turns + pSoldier->RetreatCounterStart(2); + + // if necessary, swap the usItem from holster into the hand position + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + return(AI_ACTION_TOSS_PROJECTILE); + } + } + // sevenfm: no Main Red AI for civilians if ( (gGameExternalOptions.fEnemyTanksCanMoveInTactical || !ARMED_VEHICLE( pSoldier )) && !(pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER)) && @@ -3641,91 +3813,93 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (ubCanMove && pSoldier->bActionPoints > APBPConstants[MAX_AP_CARRIED]) { - if (fCivilian) + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude)); + // calculate initial points for watch based on highest watch loc + + bWatchPts = GetHighestWatchedLocPoints(pSoldier->ubID); + if (bWatchPts <= 0) { - // only interested in hiding out... - bSeekPts = -99; - bHelpPts = -99; - bHidePts = +1; + // no watching + bWatchPts = -99; } - else + + // modify RED movement tendencies according to morale + switch (pSoldier->aiData.bAIMorale) { + case MORALE_HOPELESS: bSeekPts = -99; bHelpPts = -99; bHidePts = +2; bWatchPts = -99; break; + case MORALE_WORRIED: bSeekPts += -2; bHelpPts += 0; bHidePts += +2; bWatchPts += 1; break; + case MORALE_NORMAL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case MORALE_CONFIDENT: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + case MORALE_FEARLESS: bSeekPts += +1; bHelpPts += 0; bHidePts = -1; bWatchPts += 0; break; + } - DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("decideactionred: checking hide/seek/help/watch points... orders = %d, attitude = %d",pSoldier->aiData.bOrders,pSoldier->aiData.bAttitude)); - // calculate initial points for watch based on highest watch loc + // modify tendencies according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: bSeekPts += -1; bHelpPts += -1; bHidePts += +1; bWatchPts += +1; break; + case ONGUARD: bSeekPts += -1; bHelpPts += 0; bHidePts += +1; bWatchPts += +1; break; + case CLOSEPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case RNDPTPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case POINTPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case FARPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case ONCALL: bSeekPts += 0; bHelpPts += +1; bHidePts += -1; bWatchPts += 0; break; + case SEEKENEMY: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += -1; break; + case SNIPER: bSeekPts += -1; bHelpPts += 0; bHidePts += +1; bWatchPts += +1; break; + } - bWatchPts = GetHighestWatchedLocPoints( pSoldier->ubID ); - if ( bWatchPts <= 0 ) + // modify tendencies according to attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: bSeekPts += -1; bHelpPts += 0; bHidePts += +2; bWatchPts += +1; break; + case BRAVESOLO: bSeekPts += +1; bHelpPts += -1; bHidePts += -1; bWatchPts += -1; break; + case BRAVEAID: bSeekPts += +1; bHelpPts += +1; bHidePts += -1; bWatchPts += -1; break; + case CUNNINGSOLO: bSeekPts += 1; bHelpPts += -1; bHidePts += +1; bWatchPts += 0; break; + case CUNNINGAID: bSeekPts += 1; bHelpPts += +1; bHidePts += +1; bWatchPts += 0; break; + case AGGRESSIVE: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + case ATTACKSLAYONLY:bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + } + + // sevenfm: snipers and soldiers with scoped guns should decide watch more often + if (AIGunScoped(pSoldier) || AICheckIsSniper(pSoldier)) + { + bWatchPts++; + } + + // sevenfm: disable watching if soldier is under fire or in dangerous place + // don't watch if some friends can see my closest opponent + if (fDangerousSpot || + InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) || + CountFriendsBlack(pSoldier) > 0) + { + // prefer hiding when in dangerous place + if (bHidePts > -90) + bWatchPts = min(bWatchPts, bHidePts - 1); + else + bWatchPts--; + } + + // sevenfm: don't watch when overcrowded and not in a building + if (!InARoom(pSoldier->sGridNo, NULL)) + { + bWatchPts -= CountNearbyFriends(pSoldier, pSoldier->sGridNo, DAY_VISION_RANGE / 8); + } + + // sevenfm: don't help if seen enemy recently or under fire + if (GuySawEnemy(pSoldier) || pSoldier->aiData.bUnderFire) + { + bHelpPts -= 10; + } + + if (pSoldier->RetreatCounterValue() > 0) + { + // no seeking when retreating + bSeekPts = -99; + // no helping when retreating + bHelpPts = -99; + + if (bHidePts > -90) { - // no watching - bWatchPts = -99; - } - - // modify RED movement tendencies according to morale - switch (pSoldier->aiData.bAIMorale) - { - case MORALE_HOPELESS: bSeekPts = -99; bHelpPts = -99; bHidePts = +2; bWatchPts = -99; break; - case MORALE_WORRIED: bSeekPts += -2; bHelpPts += 0; bHidePts += +2; bWatchPts += 1; break; - case MORALE_NORMAL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; - case MORALE_CONFIDENT: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; - case MORALE_FEARLESS: bSeekPts += +1; bHelpPts += 0; bHidePts = -1; bWatchPts += 0; break; - } - - // modify tendencies according to orders - switch (pSoldier->aiData.bOrders) - { - case STATIONARY: bSeekPts += -1; bHelpPts += -1; bHidePts += +1; bWatchPts += +1; break; - case ONGUARD: bSeekPts += -1; bHelpPts += 0; bHidePts += +1; bWatchPts += +1; break; - case CLOSEPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; - case RNDPTPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; - case POINTPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; - case FARPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; - case ONCALL: bSeekPts += 0; bHelpPts += +1; bHidePts += -1; bWatchPts += 0; break; - case SEEKENEMY: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += -1; break; - case SNIPER: bSeekPts += -1; bHelpPts += 0; bHidePts += +1; bWatchPts += +1; break; - } - - // modify tendencies according to attitude - switch (pSoldier->aiData.bAttitude) - { - case DEFENSIVE: bSeekPts += -1; bHelpPts += 0; bHidePts += +2; bWatchPts += +1; break; - case BRAVESOLO: bSeekPts += +1; bHelpPts += -1; bHidePts += -1; bWatchPts += -1; break; - case BRAVEAID: bSeekPts += +1; bHelpPts += +1; bHidePts += -1; bWatchPts += -1; break; - case CUNNINGSOLO: bSeekPts += 1; bHelpPts += -1; bHidePts += +1; bWatchPts += 0; break; - case CUNNINGAID: bSeekPts += 1; bHelpPts += +1; bHidePts += +1; bWatchPts += 0; break; - case AGGRESSIVE: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; - case ATTACKSLAYONLY:bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; - } - - // sevenfm: snipers and soldiers with scoped guns should decide watch more often - if (AIGunScoped(pSoldier) || AICheckIsSniper(pSoldier)) - { - bWatchPts++; - } - - // sevenfm: disable watching if soldier is under fire or in dangerous place - // don't watch if some friends can see my closest opponent - if (fDangerousSpot || - InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) || - CountFriendsBlack(pSoldier) > 0 ) - { - // prefer hiding when in dangerous place - if (bHidePts > -90) - bWatchPts = min(bWatchPts, bHidePts - 1); - else - bWatchPts--; - } - - // sevenfm: don't watch when overcrowded and not in a building - if( !InARoom(pSoldier->sGridNo, NULL) ) - { - bWatchPts -= CountNearbyFriends(pSoldier, pSoldier->sGridNo, DAY_VISION_RANGE / 8); - } - - // sevenfm: don't help if seen enemy recently or under fire - if( GuySawEnemy(pSoldier) || pSoldier->aiData.bUnderFire ) - { - bHelpPts -= 10; + bWatchPts = min(bWatchPts, bHidePts - 1); } } @@ -4113,7 +4287,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if HIDING is possible and at least as desirable as seeking or helping if ((bHidePts > -90) && (bHidePts >= bSeekPts) && (bHidePts >= bHelpPts) && (bHidePts >= bWatchPts )) { - sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); + //sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); // if an opponent is known (not necessarily reachable or conscious) if (!SkipCoverCheck && !TileIsOutOfBounds(sClosestOpponent)) { @@ -4169,7 +4343,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////////// // if we're currently under fire (presumably, attacker is hidden) - if (pSoldier->aiData.bUnderFire || fCivilian) + if (pSoldier->aiData.bUnderFire) { // only try to run if we've actually been hit recently & noticably so // otherwise, presumably our current cover is pretty good & sufficient @@ -4189,7 +4363,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) bShock = pSoldier->aiData.bShock; } - if (bShock > 0 || fCivilian) + if (bShock > 0) { // look for best place to RUN AWAY to (farthest from the closest threat) pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); @@ -4212,36 +4386,63 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: crouch or go prone"); // if not in water and not already crouched - if (!fCivilian ) + if (gAnimControl[pSoldier->usAnimState].ubHeight == ANIM_STAND && IsValidStance(pSoldier, ANIM_CROUCH)) { - if ( gAnimControl[ pSoldier->usAnimState ].ubHeight == ANIM_STAND && IsValidStance( pSoldier, ANIM_CROUCH ) ) + if (!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) { - if (!gfTurnBasedAI || GetAPsToChangeStance( pSoldier, ANIM_CROUCH ) <= pSoldier->bActionPoints) - { #ifdef DEBUGDECISIONS - sprintf(tempstr,"%s CROUCHES (STATUS RED)",pSoldier->name); - AIPopMessage(tempstr); + sprintf(tempstr, "%s CROUCHES (STATUS RED)", pSoldier->name); + AIPopMessage(tempstr); #endif - pSoldier->aiData.usActionData = ANIM_CROUCH; - return(AI_ACTION_CHANGE_STANCE); - } + pSoldier->aiData.usActionData = ANIM_CROUCH; + return(AI_ACTION_CHANGE_STANCE); } - else if ( gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_PRONE ) + } + else if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE) + { + // maybe go prone + if (PreRandom(2) == 0 && IsValidStance(pSoldier, ANIM_PRONE)) { - // maybe go prone - if ( PreRandom( 2 ) == 0 && IsValidStance( pSoldier, ANIM_PRONE ) ) - { - pSoldier->aiData.usActionData = ANIM_PRONE; - return( AI_ACTION_CHANGE_STANCE ); - } - + pSoldier->aiData.usActionData = ANIM_PRONE; + return(AI_ACTION_CHANGE_STANCE); } } } } + // civilians are only interested in running away + if (fCivilian && + //pSoldier->CheckInitialAP() && + (pSoldier->aiData.bUnderFire || + !TileIsOutOfBounds(sClosestOpponent) && PythSpacesAway(pSoldier->sGridNo, sClosestOpponent) < DAY_VISION_RANGE / 2)) + //!TileIsOutOfBounds( sClosestNoise ) && PythSpacesAway(pSoldier->sGridNo, sClosestNoise) < DAY_VISION_RANGE / 2) ) + //CorpseWarning(pSoldier, pSoldier->sGridNo, pSoldier->pathing.bLevel) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[civilians run away]")); + + // look for best place to RUN AWAY to (farthest from the closest threat) + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + DebugAI(AI_MSG_INFO, pSoldier, String("found run away spot %d", pSoldier->aiData.usActionData)); + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_RUN_AWAY); + } + //else if (!pSoldier->SkipCoverCheck() && gfTurnBasedAI) // only do in turnbased + else if (!SkipCoverCheck && gfTurnBasedAI) // only do in turnbased + { + // try to take cover + pSoldier->aiData.bAIMorale = MORALE_WORRIED; + pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, MORALE_WORRIED, &iDummy); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_TAKE_COVER); + } + } + } + DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: look around towards opponent"); //////////////////////////////////////////////////////////////////////////// // LOOK AROUND TOWARD CLOSEST KNOWN OPPONENT, IF KNOWN @@ -4251,7 +4452,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { // determine the location of the known closest opponent // (don't care if he's conscious, don't care if he's reachable at all) - sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + //sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); if (!TileIsOutOfBounds(sClosestOpponent)) { @@ -4485,7 +4686,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if not in water and not already crouched, try to crouch down first if (!fCivilian && !bInWater && (gAnimControl[ pSoldier->usAnimState ].ubHeight == ANIM_STAND) && IsValidStance( pSoldier, ANIM_CROUCH ) ) { - sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + //sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); //if ( ( !TileIsOutOfBounds(sClosestOpponent) && PythSpacesAway( pSoldier->sGridNo, sClosestOpponent ) < (MaxNormalDistanceVisible() * 3) / 2 ) || PreRandom( 4 ) == 0 ) if ( (!TileIsOutOfBounds(sClosestOpponent) && PythSpacesAway( pSoldier->sGridNo, sClosestOpponent ) < (pSoldier->GetMaxDistanceVisible(sClosestOpponent) * 3) / 2 ) || PreRandom( 4 ) == 0 ) @@ -5024,6 +5225,60 @@ INT16 ubMinAPCost; } } + DebugAI(AI_MSG_TOPIC, pSoldier, String("[self smoke when under fire]")); + if (SoldierAI(pSoldier) && + gfTurnBasedAI && + pSoldier->bActionPoints == pSoldier->bInitialActionPoints && + pSoldier->aiData.bUnderFire && + !InARoom(pSoldier->sGridNo, NULL) && + !InSmoke(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + RangeChangeDesire(pSoldier) <= 2 && + (!NightLight() || InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel)) && + !TileIsOutOfBounds(sClosestOpponent) && + PythSpacesAway(pSoldier->sGridNo, sClosestOpponent) > DAY_VISION_RANGE / 4 && + (!ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE) && !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->TakenLargeHit()) && + (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("check if soldier can cover himself with smoke")); + + CheckTossSelfSmoke(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + + // start retreating for several turns + pSoldier->RetreatCounterStart(3); + + // if necessary, swap the usItem from holster into the hand position + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + return(AI_ACTION_TOSS_PROJECTILE); + } + } + // if we don't have a gun, look around for a weapon! if (FindAIUsableObjClass( pSoldier, IC_GUN ) == ITEM_NOT_FOUND && ubCanMove && !pSoldier->aiData.bNeutral) { @@ -5580,10 +5835,37 @@ INT16 ubMinAPCost; memset( &BestAttack, 0, sizeof( BestAttack ) ); break; } - } + } - // NB a desire of 4 or more is only achievable by brave/aggressive guys with high morale UINT16 usRange = BestAttack.bWeaponIn==NO_SLOT ? 0 : GetModifiedGunRange(pSoldier->inv[BestAttack.bWeaponIn].usItem);//dnl ch69 150913 + INT32 sClosestThreat = ClosestKnownOpponent(pSoldier, NULL, NULL); + + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black Retreat]")); + if (gfTurnBasedAI && + !bInWater && + ubCanMove && + !gfHiddenInterrupt && + !gTacticalStatus.fInterruptOccurred && + SoldierAI(pSoldier) && + pSoldier->aiData.bOrders != STATIONARY && + pSoldier->aiData.bOrders != SNIPER && + pSoldier->RetreatCounterValue() > 0 && + (ubBestAttackAction == AI_ACTION_NONE || ubBestAttackAction == AI_ACTION_FIRE_GUN && (UINT8)BestAttack.ubChanceToReallyHit < Random(10 + pSoldier->ShockLevelPercent() / 4)) && + (pSoldier->CheckInitialAP() || !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->aiData.bUnderFire)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("search for retreat spot")); + INT32 sRetreatSpot = FindRetreatSpot(pSoldier); + + if (!TileIsOutOfBounds(sRetreatSpot)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("found retreat spot %d", sRetreatSpot)); + + //BeginMultiPurposeLocator(sRetreatSpot, pSoldier->pathing.bLevel, FALSE); + + pSoldier->aiData.usActionData = sRetreatSpot; + return(AI_ACTION_TAKE_COVER); + } + } if ( (pSoldier->bActionPoints == pSoldier->bInitialActionPoints) && (ubBestAttackAction == AI_ACTION_FIRE_GUN) && @@ -6309,7 +6591,7 @@ L_NEWAIM: } else if (pSoldier->bBreath < OKBREATH || pSoldier->bBreath < pSoldier->bBreathMax && - (pSoldier->bBreath < MercPtrs[ubOpponentID]->bBreath || !pSoldier->aiData.bLastAttackHit && (pSoldier->usSoldierFlagMask2 & SOLDIER_TAKEN_LARGE_HIT))) + (pSoldier->bBreath < MercPtrs[ubOpponentID]->bBreath || !pSoldier->aiData.bLastAttackHit && pSoldier->TakenLargeHit())) { // maybe move away from opponent UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, sClosestOpponent); @@ -8679,7 +8961,7 @@ INT8 ArmedVehicleDecideActionRed( SOLDIERTYPE *pSoldier) if ( pSoldier->aiData.bUnderFire ) { // Flugente: see if we are equipped with a smoke screen. If so, use it do hide us - if ( (pSoldier->usSoldierFlagMask2 & SOLDIER_TAKEN_LARGE_HIT) && pSoldier->HasItem( SMOKE_GRENADE ) && IsActionAffordable( pSoldier, AI_ACTION_SELFDETONATE ) ) + if (pSoldier->TakenLargeHit() && pSoldier->HasItem(SMOKE_GRENADE) && IsActionAffordable(pSoldier, AI_ACTION_SELFDETONATE)) { pSoldier->aiData.usActionData = SMOKE_GRENADE; @@ -9774,6 +10056,7 @@ void LogDecideInfo(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_INFO, pSoldier, String("What I know %d", WhatIKnowThatPublicDont(pSoldier, FALSE))); DebugAI(AI_MSG_INFO, pSoldier, String("Has Gun %d, Short range weapon %d, Gun Range %d, Gun Ammo %d, Gun Scoped %d ", AICheckHasGun(pSoldier), AICheckShortWeaponRange(pSoldier), AIGunRange(pSoldier), AIGunAmmo(pSoldier), AIGunScoped(pSoldier))); + DebugAI(AI_MSG_INFO, pSoldier, String("RetreatCounter %d", pSoldier->RetreatCounterValue())); } void LogKnowledgeInfo(SOLDIERTYPE *pSoldier) diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 4fd3f6878..b93446b94 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -680,7 +680,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB } } - iBestCoverValue = -1; #if defined( _DEBUG ) && defined( COVER_DEBUG ) @@ -755,111 +754,11 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB return(NOWHERE); } - // those within 20 tiles of any tile we'll CONSIDER as cover are important - iMaxThreatRange = MAX_THREAT_RANGE + (CELL_X_SIZE * iSearchRange); - // calculate OUR OWN general threat value (not from any specific location) - iMyThreatValue = CalcManThreatValue(pSoldier,NOWHERE,FALSE,pSoldier); + iMyThreatValue = CalcManThreatValue(pSoldier, NOWHERE, FALSE, pSoldier); - // 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; - - // if this opponent is unknown personally and publicly - if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) - { - continue; // next merc - } - - // Special stuff for Carmen the bounty hunter - if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY) - { - continue; // next opponent - } - - // 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; - iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE]; - } - else - { - // using public knowledge, obtain opponent's "best guess" gridno - sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]; - iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE]; - } - - // calculate how far away this threat is (in adjusted pixels) - //iThreatRange = AdjPixelsAway(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(sThreatLoc),CenterY(sThreatLoc)); - iThreatRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sThreatLoc ); - - //NumMessage("Threat Range = ",iThreatRange); - -#ifdef DEBUGCOVER -// DebugAI( String( "FBNC: Opponent %d believed to be at gridno %d (mine %d, public %d)\n",iLoop,sThreatLoc,*pusLastLoc,PublicLastKnownOppLoc[pSoldier->bTeam][iLoop] ) ); -#endif - - // if this opponent is believed to be too far away to really be a threat - if (iThreatRange > iMaxThreatRange) - { -#ifdef DEBUGCOVER -// AINameMessage(pOpponent,"is too far away to be a threat",1000); -#endif - - 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) - { - //NameMessage(pOpponent,"is thought to be no threat"); - continue; // check next opponent - } - - //NameMessage(pOpponent,"added to the list of threats"); - //NumMessage("His/Her threat value = ",threatValue[uiThreatCnt]); - - 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(); - - if (iThreatRange < iClosestThreatRange) - { - iClosestThreatRange = iThreatRange; - //NumMessage("New Closest Threat Range = ",iClosestThreatRange); -// sClosestThreatGridNo = sThreatLoc; - //NumMessage("Closest Threat Gridno = ",sClosestThreatGridNo); - } - - uiThreatCnt++; - } + // prepare threat list from known enemies + PrepareThreatlist(pSoldier); // if no known opponents were found to threaten us, can't worry about cover if (!uiThreatCnt) diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index 82dab47d1..7bd8e8c81 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -294,9 +294,11 @@ UINT8 CountFriendsInDirection( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo ); BOOLEAN GuySawEnemy( SOLDIERTYPE * pSoldier, UINT8 ubMax = SEEN_3_TURNS_AGO ); UINT8 CountNearbyFriends( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ); UINT8 CountNearbyFriendsLastAttackHit( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ); -UINT8 CountFriendsFlankSameSpot( SOLDIERTYPE *pSoldier ); +UINT8 CountFriendsFlankSameSpot(SOLDIERTYPE *pSoldier, INT32 sSpot = NOWHERE); UINT8 CountFriendsBlack( SOLDIERTYPE *pSoldier, INT32 sClosestOpponent = NOWHERE ); UINT8 CountTeamUnderAttack(INT8 bTeam, INT32 sGridNo, INT16 sDistance); +UINT8 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT16 sDistance); +UINT8 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier); UINT8 SectorCurfew(BOOLEAN fNight); UINT8 TeamPercentKilled(INT8 bTeam); @@ -311,6 +313,9 @@ BOOLEAN AICheckFriendsNoContact( SOLDIERTYPE *pSoldier ); BOOLEAN AICheckIsFlanking( SOLDIERTYPE *pSoldier ); INT8 CalcMoraleNew(SOLDIERTYPE *pSoldier); +void PrepareThreatlist(SOLDIERTYPE *pSoldier); +UINT8 ClosestSeenThreatID(SOLDIERTYPE *pSoldier, UINT8 ubMax = SEEN_CURRENTLY); // first call PrepareThreatlist to make threat list +UINT8 ClosestKnownThreatID(SOLDIERTYPE *pSoldier); // first call PrepareThreatlist to make threat list BOOLEAN ProneSightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited); BOOLEAN SightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited); @@ -396,6 +401,7 @@ INT8 KnownPublicLevel(UINT8 bTeam, UINT8 ubOpponentID); #define MAX_TILES_MOVE_TURN (APBPConstants[AP_MAXIMUM] / (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_RUN])) // vision range defines +#define MAX_VISION_RANGE (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO * 4) #define DAY_VISION_RANGE (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO * 2) #define NIGHT_VISION_RANGE (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO ) #define VISION_RANGE MaxNormalVisionDistance()