diff --git a/GameSettings.cpp b/GameSettings.cpp index ed6dd18e..5110a63d 100644 --- a/GameSettings.cpp +++ b/GameSettings.cpp @@ -1477,6 +1477,12 @@ void LoadGameExternalOptions() // New enemy AI for night battles gGameExternalOptions.bNewTacticalAIBehavior = iniReader.ReadBoolean("Tactical Interface Settings","NEW_AI_TACTICAL",FALSE); + + // sevenfm: AI tweaks and improvements + gGameExternalOptions.fAIExtraSuppression = iniReader.ReadBoolean("Tactical Interface Settings","AI_EXTRA_SUPPRESSION",FALSE); + gGameExternalOptions.fAINewMorale = iniReader.ReadBoolean("Tactical Interface Settings","AI_NEW_MORALE",FALSE); + gGameExternalOptions.fAIBetterCover = iniReader.ReadBoolean("Tactical Interface Settings","AI_BETTER_COVER",FALSE); + gGameExternalOptions.fAIExtraFlanking = iniReader.ReadBoolean("Tactical Interface Settings","AI_EXTRA_FLANKING",FALSE); // Heahshot penalty gGameExternalOptions.uShotHeadPenalty = iniReader.ReadFloat("Tactical Interface Settings","SHOT_HEAD_PENALTY",3,0,100); diff --git a/GameSettings.h b/GameSettings.h index 824c85f1..c2c1449b 100644 --- a/GameSettings.h +++ b/GameSettings.h @@ -764,6 +764,10 @@ typedef struct BOOLEAN bAllowSpecialExplosiveAttachments; INT16 iChanceSayAnnoyingPhrase; BOOLEAN bNewTacticalAIBehavior; + BOOLEAN fAIExtraSuppression; + BOOLEAN fAINewMorale; + BOOLEAN fAIBetterCover; + BOOLEAN fAIExtraFlanking; FLOAT uShotHeadPenalty; FLOAT fShotHeadMultiplier; INT16 iPenaltyShootUnSeen; diff --git a/GameVersion.cpp b/GameVersion.cpp index 39c96e26..39bec8fe 100644 --- a/GameVersion.cpp +++ b/GameVersion.cpp @@ -15,9 +15,9 @@ #ifdef JA2EDITOR #ifdef JA2UB - CHAR16 zVersionLabel[256] = { L"Unfinished Business - Map Editor v1.13.8005 (Development Build)" }; + CHAR16 zVersionLabel[256] = { L"Unfinished Business - Map Editor v1.13.8006 (Development Build)" }; #else - CHAR16 zVersionLabel[256] = { L"Map Editor v1.13.8005 (Development Build)" }; + CHAR16 zVersionLabel[256] = { L"Map Editor v1.13.8006 (Development Build)" }; #endif // ------------------------------ @@ -27,11 +27,11 @@ //DEBUG BUILD VERSION #ifdef JA2UB - CHAR16 zVersionLabel[256] = { L"Debug: Unfinished Business - v1.13.8005 (Development Build)" }; + CHAR16 zVersionLabel[256] = { L"Debug: Unfinished Business - v1.13.8006 (Development Build)" }; #elif defined (JA113DEMO) - CHAR16 zVersionLabel[256] = { L"Debug: JA2 Demo - v1.13.8005 (Development Build)" }; + CHAR16 zVersionLabel[256] = { L"Debug: JA2 Demo - v1.13.8006 (Development Build)" }; #else - CHAR16 zVersionLabel[256] = { L"Debug: v1.13.8005 (Development Build)" }; + CHAR16 zVersionLabel[256] = { L"Debug: v1.13.8006 (Development Build)" }; #endif #elif defined CRIPPLED_VERSION @@ -46,16 +46,16 @@ //RELEASE BUILD VERSION #ifdef JA2UB - CHAR16 zVersionLabel[256] = { L"Release Unfinished Business - v1.13.8005 (Development Build)" }; + CHAR16 zVersionLabel[256] = { L"Release Unfinished Business - v1.13.8006 (Development Build)" }; #elif defined (JA113DEMO) - CHAR16 zVersionLabel[256] = { L"Release JA2 Demo - v1.13.8005 (Development Build)" }; + CHAR16 zVersionLabel[256] = { L"Release JA2 Demo - v1.13.8006 (Development Build)" }; #else - CHAR16 zVersionLabel[256] = { L"Release v1.13.8005 (Development Build)" }; + CHAR16 zVersionLabel[256] = { L"Release v1.13.8006 (Development Build)" }; #endif #endif -CHAR8 czVersionNumber[16] = { "Build 15.12.14" }; //YY.MM.DD +CHAR8 czVersionNumber[16] = { "Build 15.12.19" }; //YY.MM.DD CHAR16 zTrackingNumber[16] = { L"Z" }; // SAVE_GAME_VERSION is defined in header, change it there diff --git a/Tactical/LOS.cpp b/Tactical/LOS.cpp index 71db90fe..c4ac9666 100644 --- a/Tactical/LOS.cpp +++ b/Tactical/LOS.cpp @@ -1804,7 +1804,10 @@ INT32 LineOfSightTest( FLOAT dStartX, FLOAT dStartY, FLOAT dStartZ, FLOAT dEndX, gLOSTestResults.ubTreeSpotsHit++; gLOSTestResults.iMaxDistance = iSightLimit; #endif - if (iDistance > iAdjSightLimit && !cthCalc) + // sevenfm: don't stop on trees if testing with no sight limit + if (iDistance > iAdjSightLimit && + !cthCalc && + iTileSightLimit < 255) { // out of visual range #ifdef LOS_DEBUG diff --git a/Tactical/Weapons.cpp b/Tactical/Weapons.cpp index 41de1671..a602dafe 100644 --- a/Tactical/Weapons.cpp +++ b/Tactical/Weapons.cpp @@ -9118,9 +9118,21 @@ UINT32 AICalcChanceToHitGun(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT16 ubAimTim pSoldier->bTargetLevel = bTargetLevel; pSoldier->usAnimState = usAnimState; if(Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE)//dnl ch70 160913 + { uiChance = CalcThrownChanceToHit(pSoldier, sGridNo, ubAimTime, ubAimPos); + } else + { uiChance = CalcChanceToHitGun(pSoldier, sGridNo, ubAimTime, ubAimPos); + + // sevenfm: allow small CTH for AI for suppression fire + if( gGameExternalOptions.fAIExtraSuppression && + pSoldier->aiData.bAlertStatus == STATUS_RED && + Weapon[ pSoldier->usAttackingWeapon ].bAutofireShotsPerFiveAP > 0 ) + { + uiChance = __max(1, uiChance); + } + } pSoldier->usAnimState = usTrueState; pSoldier->bTargetLevel = bTrueLevel; if(UsingNewCTHSystem() == true && !(Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE))//dnl ch70 160913 diff --git a/Tactical/opplist.cpp b/Tactical/opplist.cpp index e424ff01..90d57076 100644 --- a/Tactical/opplist.cpp +++ b/Tactical/opplist.cpp @@ -7501,9 +7501,9 @@ INT8 FindUnusedWatchedLoc( UINT8 ubID ) for ( bLoop = 0; bLoop < NUM_WATCHED_LOCS; bLoop++ ) { // WANNE: I think this was a bug, should be != NOWHERE! - //if ( gsWatchedLoc[ ubID ][ bLoop ] == NOWHERE ) - - if (!TileIsOutOfBounds(gsWatchedLoc[ ubID ][ bLoop ])) + // sevenfm: the original code is ok, as we search for unused WatchedLoc which is initialized with NOWHERE value + if ( gsWatchedLoc[ ubID ][ bLoop ] == NOWHERE ) + //if (!TileIsOutOfBounds(gsWatchedLoc[ ubID ][ bLoop ])) { return( bLoop ); } diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index dcecb431..3abd66a7 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -1050,6 +1050,9 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK, } } + // sevenfm: only if not found enemy + if ( sClosestDisturbance == NOWHERE ) + { // if any "misc. noise" was also heard recently if (!TileIsOutOfBounds(pSoldier->aiData.sNoiseGridno) && pSoldier->aiData.sNoiseGridno != sClosestDisturbance) { @@ -1077,7 +1080,11 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK, // get the AP cost to get to the location of the noise iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo ); // if we can get there - if (iPathCost != 0) + // sevenfm: check that noise is closer than known disturbance + //if (iPathCost != 0) + if (iPathCost != 0 && + PythSpacesAway( pSoldier->sGridNo, sGridNo ) < sDistToClosestEnemy && + iPathCost < iShortestPath) { if (fClimbingNecessary) { @@ -1109,7 +1116,11 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK, // get the AP cost to get to the location of the noise iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo ); // if we can get there - if (iPathCost != 0) + // sevenfm: check that noise is closer than known disturbance + //if (iPathCost != 0) + if (iPathCost != 0 && + PythSpacesAway( pSoldier->sGridNo, sGridNo ) < sDistToClosestEnemy && + iPathCost < iShortestPath) { if (fClimbingNecessary) { @@ -1132,6 +1143,7 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK, (*pubNoiseVolume)--; } } + } // sevenfm: end check enemy closestdisturbance #ifdef DEBUGDECISIONS if (!TileIsOutOfBounds(sClosestDisturbance)) @@ -2067,6 +2079,12 @@ INT8 CalcMorale(SOLDIERTYPE *pSoldier) INT8 *pbPersOL, *pbPublOL; SOLDIERTYPE *pOpponent,*pFriend; + // sevenfm: use new calculation: + if( gGameExternalOptions.fAINewMorale ) + { + return CalcMoraleNew( pSoldier ); + } + // if army guy has NO weapons left then panic! if ( pSoldier->bTeam == ENEMY_TEAM ) { @@ -3273,4 +3291,494 @@ UINT8 CountFriendsInDirection( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo ) } return ubFriends; +} + +// sevenfm: check if suppression is possible (count friends in the fire direction) +BOOLEAN CheckSuppressionDirection( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo ) +{ + SOLDIERTYPE * pFriend; + UINT8 ubShootingDir, ubFriendDir; + + // safety check + if( !pSoldier ) + return TRUE; + + ubShootingDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(sTargetGridNo),CenterY(sTargetGridNo)); + + // Run through each friendly. + for ( UINT8 iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; iCounter ++ ) + { + pFriend = MercPtrs[ iCounter ]; + ubFriendDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(pFriend->sGridNo),CenterY(pFriend->sGridNo)); + + if (pFriend != pSoldier && + pFriend->bActive && + pFriend->stats.bLife >= OKLIFE && + pSoldier->pathing.bLevel == pFriend->pathing.bLevel && + ubShootingDir == ubFriendDir && + PythSpacesAway( pSoldier->sGridNo, pFriend->sGridNo) < PythSpacesAway(pSoldier->sGridNo, sTargetGridNo) && + CalcAverageCTGTForPosition( pSoldier, pFriend->ubID, pFriend->sGridNo, pFriend->pathing.bLevel, pSoldier->bActionPoints ) > 0 && + ( gAnimControl[ pFriend->usAnimState ].ubHeight != ANIM_PRONE || + PythSpacesAway( pSoldier->sGridNo, pFriend->sGridNo ) <= 15 ) ) + { + return FALSE; + } + } + + return TRUE; +} + +// sevenfm: count nearby friend soldiers +UINT8 CountNearbyFriendlies( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ) +{ + SOLDIERTYPE * pFriend; + UINT8 ubFriendCount = 0; + + // safety check + if( !pSoldier ) + return 0; + + // Run through each friendly. + for ( UINT8 iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; iCounter ++ ) + { + pFriend = MercPtrs[ iCounter ]; + // Make sure that character is alive, not too shocked, and conscious, and of higher experience level + // than the character being suppressed. + if (pFriend != pSoldier && pFriend->bActive && pFriend->stats.bLife >= OKLIFE && + PythSpacesAway( sGridNo, pFriend->sGridNo ) <= ubDistance ) + { + ubFriendCount++; + } + } + + return ubFriendCount; +} + +// sevenfm: new AI morale calculation +INT8 CalcMoraleNew(SOLDIERTYPE *pSoldier) +{ + UINT32 uiLoop, uiLoop2; + INT32 iOurTotalThreat = 0, iTheirTotalThreat = 0; + INT16 sOppThreatValue, sFrndThreatValue, sMorale; + INT32 iPercent; + INT8 bMostRecentOpplistValue; + INT8 bMoraleCategory; + UINT8 *pSeenOpp; //,*friendOlPtr; + INT8 *pbPersOL, *pbPublOL; + SOLDIERTYPE *pOpponent,*pFriend; + + // if army guy has NO weapons left then panic! + if ( pSoldier->bTeam == ENEMY_TEAM ) + { + if ( FindAIUsableObjClass( pSoldier, IC_WEAPON ) == NO_SLOT ) + { + // sevenfm: disable as we can pick up weapon or attack with fists + //return( MORALE_HOPELESS ); + } + } + + // hang pointers to my personal opplist, my team's public opplist, and my + // list of previously seen opponents + pSeenOpp = (UINT8 *) &(gbSeenOpponents[pSoldier->ubID][0]); + + // loop through every one of my possible opponents + for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) + { + pOpponent = MercSlots[ uiLoop ]; + + // if this merc is inactive, at base, on assignment, dead, unconscious + if (!pOpponent || (pOpponent->stats.bLife < OKLIFE)) + continue; // next merc + + // if this merc is neutral/on same side, he's not an opponent, skip him! + if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide)) + continue; // next merc + + // Special stuff for Carmen the bounty hunter + if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY) + { + continue; // next opponent + } + + pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; + pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; + pSeenOpp = (UINT8 *)gbSeenOpponents[pSoldier->ubID] + pOpponent->ubID; + + // if this opponent is unknown to me personally AND unknown to my team, too + if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) + { + // if I have never seen him before anywhere in this sector, either + if (!(*pSeenOpp)) + continue; // next merc + + // have seen him in the past, so he remains something of a threat + bMostRecentOpplistValue = 0; // uses the free slot for 0 opplist + } + else // decide which opplist is more current + { + // if personal knowledge is more up to date or at least equal + if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL)) + bMostRecentOpplistValue = *pbPersOL; // use personal + else + bMostRecentOpplistValue = *pbPublOL; // use public + } + + iPercent = ThreatPercent[bMostRecentOpplistValue - OLDEST_HEARD_VALUE]; + + sOppThreatValue = (iPercent * CalcManThreatValue(pOpponent,pSoldier->sGridNo,FALSE,pSoldier)) / 100; + + //sprintf(tempstr,"Known opponent %s, opplist status %d, percent %d, threat = %d", + // ExtMen[pOpponent->ubID].name,ubMostRecentOpplistValue,ubPercent,sOppThreatValue); + //PopMessage(tempstr); + + // ADD this to their running total threatValue (decreases my MORALE) + iTheirTotalThreat += sOppThreatValue; + //NumMessage("Their TOTAL threat now = ",sTheirTotalThreat); + + // NOW THE FUN PART: SINCE THIS OPPONENT IS KNOWN TO ME IN SOME WAY, + // ANY FRIENDS OF MINE THAT KNOW ABOUT HIM BOOST MY MORALE. SO, LET'S GO + // THROUGH THEIR PERSONAL OPPLISTS AND CHECK WHICH OF MY FRIENDS KNOW + // SOMETHING ABOUT HIM AND WHAT THEIR THREAT VALUE TO HIM IS. + + for (uiLoop2 = 0; uiLoop2 < guiNumMercSlots; uiLoop2++) + { + pFriend = MercSlots[ uiLoop2 ]; + + // if this merc is inactive, at base, on assignment, dead, unconscious + if (!pFriend || (pFriend->stats.bLife < OKLIFE)) + continue; // next merc + + // if this merc is not on my side, then he's NOT one of my friends + + // WE CAN'T AFFORD TO CONSIDER THE ENEMY OF MY ENEMY MY FRIEND, HERE! + // ONLY IF WE ARE ACTUALLY OFFICIALLY CO-OPERATING TOGETHER (SAME SIDE) + if ( pFriend->aiData.bNeutral && !( pSoldier->ubCivilianGroup != NON_CIV_GROUP && pSoldier->ubCivilianGroup == pFriend->ubCivilianGroup ) ) + { + continue; // next merc + } + + if ( pSoldier->bSide != pFriend->bSide ) + continue; // next merc + + // THIS TEST IS INVALID IF A COMPUTER-TEAM IS PLAYING CO-OPERATIVELY + // WITH A NON-COMPUTER TEAM SINCE THE OPPLISTS INVOLVED ARE NOT + // UP-TO-DATE. THIS SITUATION IS CURRENTLY NOT POSSIBLE IN HTH/DG. + + // ALSO NOTE THAT WE COUNT US AS OUR (BEST) FRIEND FOR THESE CALCULATIONS + + // subtract HEARD_2_TURNS_AGO (which is negative) to make values start at 0 and + // be positive otherwise + iPercent = ThreatPercent[pFriend->aiData.bOppList[pOpponent->ubID] - OLDEST_HEARD_VALUE]; + + // reduce the percentage value based on how far away they are from the enemy, if they only hear him + if ( pFriend->aiData.bOppList[ pOpponent->ubID ] <= HEARD_LAST_TURN ) + { + iPercent -= PythSpacesAway( pSoldier->sGridNo, pFriend->sGridNo ) * 2; + if ( iPercent <= 0 ) + { + //ignore! + continue; + } + } + + sFrndThreatValue = (iPercent * CalcManThreatValue(pFriend,pOpponent->sGridNo,FALSE,pSoldier)) / 100; + + // ADD this to our running total threatValue (increases my MORALE) + // We multiply by sOppThreatValue to PRO-RATE this based on opponent's + // threat value to ME personally. Divide later by sum of them all. + iOurTotalThreat += sOppThreatValue * sFrndThreatValue; + } + } + + // if they are no threat whatsoever + if (!iTheirTotalThreat) + sMorale = 500; // our morale is just incredible + else + { + // now divide sOutTotalThreat by sTheirTotalThreat to get the REAL value + iOurTotalThreat /= iTheirTotalThreat; + + // calculate the morale (100 is even, < 100 is us losing, > 100 is good) + sMorale = (INT16) ((100 * iOurTotalThreat) / iTheirTotalThreat); + } + + if (sMorale <= 25) // odds 1:4 or worse + bMoraleCategory = MORALE_HOPELESS; + else if (sMorale <= 50) // odds between 1:4 and 1:2 + bMoraleCategory = MORALE_WORRIED; + else if (sMorale <= 150) // odds between 1:2 and 3:2 + bMoraleCategory = MORALE_NORMAL; + else if (sMorale <= 300) // odds between 3:2 and 3:1 + bMoraleCategory = MORALE_CONFIDENT; + else // odds better than 3:1 + bMoraleCategory = MORALE_FEARLESS; + + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: bMoraleCategory--; break; + case BRAVESOLO: bMoraleCategory += 2; break; + case BRAVEAID: bMoraleCategory += 2; break; + case CUNNINGSOLO: break; + case CUNNINGAID: break; + case AGGRESSIVE: bMoraleCategory++; break; + } + + // sevenfm: new calculation + + // make idiot administrators more aggressive + if ( pSoldier->ubSoldierClass == SOLDIER_CLASS_ADMINISTRATOR ) + { + bMoraleCategory += 2; + } + + // if have good health + if( pSoldier->stats.bLife > 3*pSoldier->stats.bLifeMax/4 ) + { + bMoraleCategory++; + } + // bad health/breath + if( pSoldier->stats.bLife < pSoldier->stats.bLifeMax/2 ) + { + bMoraleCategory--; + } + if( pSoldier->bBreath < pSoldier->bBreathMax/2 ) + { + bMoraleCategory--; + } + + // if not under fire - attack + if( !pSoldier->aiData.bUnderFire ) + bMoraleCategory++; + + // if last attack of this soldier hit enemy - increase morale + if( pSoldier->aiData.bLastAttackHit ) + { + bMoraleCategory++; + } + + // sevenfm: wait for flanking soldiers, if had contact with enemy recently and not overcrowded here + if( GuySawEnemyThisTurnOrBefore(pSoldier) ) + //CountNearbyFriendlies(pSoldier, pSoldier->sGridNo, 5) < 3 ) + { + // count friends that flank around the same spot + bMoraleCategory -= CountFriendsFlankSeek( pSoldier ); + + // check (mobile) friends nearby that had no contact with enemy recently + if( AICheckFriendsNoContact(pSoldier) ) + { + bMoraleCategory--; + } + } + + // if some friend hit enemy - increase morale + if( CountNearbyFriendliesLastAttackHit(pSoldier, pSoldier->sGridNo, MaxNormalVisionDistance() / 2) > 0 ) + { + bMoraleCategory++; + } + + // limit morale for wounded/low breath soldiers + if (pSoldier->bBreath < pSoldier->bBreathMax/2 || + pSoldier->stats.bLife < pSoldier->stats.bLifeMax/2) + { + bMoraleCategory = __min( bMoraleCategory, MORALE_CONFIDENT ); + } + + INT8 bMaxMoraleCategory; + // sevenfm: limit morale depending on orders - snipers, stationary and guards should not charge enemy + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: + case ONGUARD: + case SNIPER: + bMaxMoraleCategory = MORALE_CONFIDENT; + break; + default: + bMaxMoraleCategory = MORALE_FEARLESS; + } + + // limit AI morale depending on morale and suppression shock + if( pSoldier->aiData.bShock ) + { + bMaxMoraleCategory = __min(bMaxMoraleCategory, (20 + pSoldier->aiData.bMorale - 20*__min(3, pSoldier->aiData.bShock/5)) / 20); + } + + // apply max limit + bMoraleCategory = __min( bMoraleCategory, bMaxMoraleCategory ); + + // sevenfm: no less than MORALE_WORRIED category if we can fight + if( pSoldier->stats.bLife > pSoldier->stats.bLifeMax/2 && + pSoldier->bBreath > pSoldier->bBreathMax/4 ) + { + bMoraleCategory = __max( bMoraleCategory, MORALE_WORRIED ); + } + + // if adjustments made it outside the allowed limits + if (bMoraleCategory < MORALE_HOPELESS) + bMoraleCategory = MORALE_HOPELESS; + else if (bMoraleCategory > MORALE_FEARLESS) + bMoraleCategory = MORALE_FEARLESS; + + return(bMoraleCategory); +} + +UINT8 CountNearbyFriendliesLastAttackHit( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ) +{ + SOLDIERTYPE * pFriend; + UINT8 ubFriendCount = 0; + + // safety check + if( !pSoldier ) + return 0; + + // Run through each friendly. + for ( UINT8 iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; iCounter ++ ) + { + pFriend = MercPtrs[ iCounter ]; + + if (pFriend != pSoldier && + pFriend->bActive && + pFriend->stats.bLife >= OKLIFE && + pFriend->aiData.bOrders > ONGUARD && + pFriend->aiData.bOrders != SNIPER && + PythSpacesAway( sGridNo, pFriend->sGridNo ) <= ubDistance && + pFriend->aiData.bLastAttackHit ) + { + ubFriendCount++; + } + } + + return ubFriendCount; +} + +UINT8 CountFriendsFlankSeek( SOLDIERTYPE *pSoldier ) +{ + SOLDIERTYPE * pFriend; + UINT8 ubFriendCount = 0; + + UINT8 ubFlankLeft = 0; + UINT8 ubFlankRight = 0; + + // safety check + if( !pSoldier ) return 0; + + UINT8 ubMaxDist = MaxNormalVisionDistance() / 2; + UINT8 ubNearbyFriendsContact = CountNearbyFriendliesContact(pSoldier, pSoldier->sGridNo, ubMaxDist); + INT32 sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); + + if(TileIsOutOfBounds(sClosestOpponent)) + { + return 0; + } + + // Run through each friendly. + for ( UINT8 iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; iCounter ++ ) + { + pFriend = MercPtrs[ iCounter ]; + + if (pFriend != pSoldier && + pFriend->bActive && + pFriend->stats.bLife >= OKLIFE && + pFriend->aiData.bAlertStatus == STATUS_RED && + pFriend->aiData.bOrders > ONGUARD && + pFriend->aiData.bOrders != SNIPER ) + { + // check if this friend flanks around the same spot + if( pFriend->numFlanks > 0 && pFriend->numFlanks < MAX_FLANKS_RED && + PythSpacesAway(pFriend->lastFlankSpot, sClosestOpponent) < ubMaxDist ) + { + if( pFriend->flags.lastFlankLeft ) + { + ubFlankLeft++; + } + else + { + ubFlankRight++; + } + } + } + } + + if(ubFlankLeft > 0 && ubFlankLeft >= ubNearbyFriendsContact/2) + { + ubFriendCount++; + } + if(ubFlankRight > 0 && ubFlankRight >= ubNearbyFriendsContact/2) + { + ubFriendCount++; + } + + return ubFriendCount; +} + +// sevenfm: count nearby friend soldiers - only soldiers who had contact with enemy recently +UINT8 CountNearbyFriendliesContact( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ) +{ + SOLDIERTYPE * pFriend; + UINT8 ubFriendCount = 0; + + // safety check + if( !pSoldier ) return 0; + + // Run through each friendly. + for ( UINT8 iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; iCounter ++ ) + { + pFriend = MercPtrs[ iCounter ]; + // Make sure that character is alive, not too shocked, and conscious, and of higher experience level + // than the character being suppressed. + if (pFriend != pSoldier && + pFriend->bActive && + pFriend->stats.bLife >= OKLIFE && + pFriend->aiData.bOrders > ONGUARD && + pFriend->aiData.bOrders != SNIPER && + PythSpacesAway( sGridNo, pFriend->sGridNo ) <= ubDistance && + GuySawEnemyThisTurnOrBefore(pFriend) ) + { + ubFriendCount++; + } + } + + return ubFriendCount; +} + +// sevenfm: count nearby friend soldiers - only soldiers who had no contact with enemy recently +UINT8 CountNearbyFriendliesNoContact( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ) +{ + SOLDIERTYPE * pFriend; + UINT8 ubFriendCount = 0; + + // safety check + if( !pSoldier ) return 0; + + // Run through each friendly. + for ( UINT8 iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; iCounter ++ ) + { + pFriend = MercPtrs[ iCounter ]; + // Make sure that character is alive, not too shocked, and conscious, and of higher experience level + // than the character being suppressed. + if (pFriend != pSoldier && + pFriend->bActive && + pFriend->stats.bLife >= OKLIFE && + pFriend->aiData.bOrders > ONGUARD && + pFriend->aiData.bOrders != SNIPER && + PythSpacesAway( sGridNo, pFriend->sGridNo ) <= ubDistance && + !GuySawEnemyThisTurnOrBefore(pFriend) ) + { + ubFriendCount++; + } + } + + return ubFriendCount; +} + +BOOLEAN AICheckFriendsNoContact( SOLDIERTYPE *pSoldier ) +{ + UINT8 ubMaxDist = MaxNormalVisionDistance() / 2; + UINT8 ubNearbyFriendsContact = CountNearbyFriendliesContact(pSoldier, pSoldier->sGridNo, ubMaxDist); + UINT8 ubNearbyFriendsNoContact = CountNearbyFriendliesNoContact(pSoldier, pSoldier->sGridNo, ubMaxDist); + + if( ubNearbyFriendsNoContact > ubNearbyFriendsContact ) + { + return TRUE; + } + return FALSE; } \ No newline at end of file diff --git a/TacticalAI/Attacks.cpp b/TacticalAI/Attacks.cpp index 2b11a9c8..c208a363 100644 --- a/TacticalAI/Attacks.cpp +++ b/TacticalAI/Attacks.cpp @@ -2124,6 +2124,20 @@ INT32 EstimateThrowDamage( SOLDIERTYPE *pSoldier, UINT8 ubItemPos, SOLDIERTYPE * iExplosDamage = ( ( (INT32) GetModifiedExplosiveDamage( Explosive[ ubExplosiveIndex ].ubDamage, 0 ) ) * 3) / 2; iBreathDamage = ( ( (INT32) GetModifiedExplosiveDamage( Explosive[ ubExplosiveIndex ].ubStunDamage, 1 ) ) * 5) / 4; + // sevenfm: IndoorModifier - increase damage inside buildings + if (gpWorldLevelData[sGridNo].ubTerrainID == FLAT_FLOOR ) + { + iExplosDamage += (INT32) (iExplosDamage * Explosive[ ubExplosiveIndex ].bIndoorModifier); + } + + // sevenfm: add damage from fragments + if ( Explosive[ ubExplosiveIndex ].ubType == EXPLOSV_NORMAL && + Explosive[ ubExplosiveIndex ].usNumFragments > 0) + { + // sevenfm: use NumFragments/10, but no more than 20 fragments + iExplosDamage += __min( 20, Explosive[ ubExplosiveIndex ].usNumFragments / 10 ) * Explosive[ ubExplosiveIndex ].ubFragDamage; + } + if ( Explosive[ ubExplosiveIndex ].ubType == EXPLOSV_TEARGAS || Explosive[ ubExplosiveIndex ].ubType == EXPLOSV_MUSTGAS ) { // if target gridno is outdoors (where tear gas lasts only 1-2 turns) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 222b8522..fa50f74a 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -36,6 +36,7 @@ #include "connect.h" #include "Text.h" #include "Exit Grids.h" // added by Flugente +#include "Game Clock.h" // sevenfm ////////////////////////////////////////////////////////////////////////////// // SANDRO - In this file, all APBPConstants[AP_CROUCH] and APBPConstants[AP_PRONE] were changed to GetAPsCrouch() and GetAPsProne() @@ -57,16 +58,6 @@ UINT32 guiRedSeekCounter = 0, guiRedHelpCounter = 0; guiRedHideCounter = 0; #define CENTER_OF_RING 11237//dnl!!! -// sevenfm: moved to ai.h -/*#define MAX_FLANKS_RED 15 -#define MAX_FLANKS_YELLOW 25 - -#define MIN_FLANK_DIST_YELLOW 10 * STRAIGHT_RATIO -#define MAX_FLANK_DIST_YELLOW 50 * STRAIGHT_RATIO - -#define MIN_FLANK_DIST_RED 10 * STRAIGHT_RATIO -#define MAX_FLANK_DIST_RED 40 * STRAIGHT_RATIO*/ - #ifdef ENABLE_ZOMBIES INT8 ZombieDecideActionGreen(SOLDIERTYPE *pSoldier); INT8 ZombieDecideActionYellow(SOLDIERTYPE *pSoldier); @@ -2101,8 +2092,9 @@ INT8 DecideActionYellow(SOLDIERTYPE *pSoldier) // STATIONARY/ONGUARD/CLOSEPATROL/SNIPER should not flank if ( ( pSoldier->aiData.bAttitude == CUNNINGAID || pSoldier->aiData.bAttitude == CUNNINGSOLO ) && CountFriendsInDirection( pSoldier, sNoiseGridNo ) > 0 && - pSoldier->aiData.bOrders > CLOSEPATROL && - pSoldier->aiData.bOrders != SNIPER ) + ( pSoldier->aiData.bOrders == SEEKENEMY || + pSoldier->aiData.bOrders == FARPATROL || + pSoldier->aiData.bOrders == CLOSEPATROL && NightTime() ) ) { INT8 action = AI_ACTION_SEEK_NOISE; INT16 dist = PythSpacesAway ( pSoldier->sGridNo, sNoiseGridNo ); @@ -2842,11 +2834,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) pSoldier->usAISkillUse = SKILLS_RADIO_JAM; pSoldier->aiData.usActionData = skilltargetgridno; return(AI_ACTION_USE_SKILL); - } + } } } - //RELOADING + //RELOADING // WarmSteel - Because of suppression fire, we need enough ammo to even consider suppressing // This means we need to reload. Also reload if we're just plainly low on bullets. if(BestShot.bWeaponIn != NO_SLOT @@ -2868,9 +2860,19 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) } //SUPPRESSION FIRE - CheckIfShotPossible(pSoldier,&BestShot,TRUE); //WarmSteel - No longer returns 0 when there IS actually a chance to hit. + // sevenfm: check that we have a clip to reload + BOOLEAN fExtraClip = FALSE; + if(BestShot.bWeaponIn != NO_SLOT) + { + INT8 bAmmoSlot = FindAmmoToReload( pSoldier, BestShot.bWeaponIn, NO_SLOT ); + if (bAmmoSlot != NO_SLOT) + { + fExtraClip = TRUE; + } + } + //must have a small chance to hit and the opponent must be on the ground (can't suppress guys on the roof) // HEADROCK HAM BETA2.4: Adjusted this for a random chance to suppress regardless of chance. This augments // current revamp of suppression fire. @@ -2883,7 +2885,16 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) && pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft >= gGameExternalOptions.ubAISuppressionMinimumAmmo && BestShot.ubChanceToReallyHit < (INT16)(PreRandom(100) - 50) && Menptr[BestShot.ubOpponent].pathing.bLevel == 0 - && pSoldier->aiData.bOrders != SNIPER ) + && pSoldier->aiData.bOrders != SNIPER && + // sevenfm: check that we'll not shoot at our friends + CheckSuppressionDirection( pSoldier, Menptr[BestShot.ubOpponent].sGridNo ) && + // sevenfm: don't suppress if target already cowering + !CoweringShockLevel(MercPtrs[BestShot.ubOpponent]) && + // sevenfm: don't suppress when flanking + (pSoldier->numFlanks == 0 || pSoldier->numFlanks > MAX_FLANKS_RED) && + // sevenfm: don't suppress if we don't have extra clip to reload + fExtraClip + ) { // then do it! @@ -3375,7 +3386,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) // sevenfm: stop flanking if no friend between me and noise gridno if ( (currDir - origDir) >= 4 || - CountFriendsInDirection( pSoldier, tempGridNo ) == 0 ) + CountFriendsInDirection( pSoldier, tempGridNo ) == 0 || + (pSoldier->aiData.bOrders != CUNNINGSOLO && + (currDir - origDir) >= 2 && + CountFriendsInDirection( pSoldier, tempGridNo ) < 2) ) { pSoldier->numFlanks = MAX_FLANKS_RED; } @@ -3396,7 +3410,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) // sevenfm: stop flanking if no friend between me and noise gridno if ( (origDir - currDir) >= 4 || - CountFriendsInDirection( pSoldier, tempGridNo ) == 0 ) + CountFriendsInDirection( pSoldier, tempGridNo ) == 0 || + (pSoldier->aiData.bOrders != CUNNINGSOLO && + (origDir - currDir) >= 2 && + CountFriendsInDirection( pSoldier, tempGridNo ) < 2) ) { pSoldier->numFlanks = MAX_FLANKS_RED; } @@ -3411,7 +3428,70 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) } } } + // sevenfm: when we finished flanking, try to reach lastFlankSpot position + // seek until we are close (DistanceVisible/2) and have line of sight to lastFlankSpot position + // don't seek if we have seen enemy recently or under fire or have shock + // don't seek if we have low AP (tired, wounded) if ( pSoldier->numFlanks == MAX_FLANKS_RED ) + { + DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: stop flanking"); + + if( !TileIsOutOfBounds(tempGridNo) && + !GuySawEnemyThisTurnOrBefore(pSoldier) && + !pSoldier->aiData.bUnderFire && + !Water(pSoldier->sGridNo) && + pSoldier->bInitialActionPoints >= APBPConstants[AP_MINIMUM] && + ( PythSpacesAway( pSoldier->sGridNo, tempGridNo ) > MIN_FLANK_DIST_RED || + !LocationToLocationLineOfSightTest( pSoldier->sGridNo, pSoldier->pathing.bLevel, tempGridNo, pSoldier->pathing.bLevel, TRUE) ) ) + { + // wait for next turn if not enough AP + if( gfTurnBasedAI && pSoldier->bActionPoints < pSoldier->bInitialActionPoints/2 ) + { + return(AI_ACTION_END_TURN); + } + + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier,tempGridNo,GetAPsCrouch( pSoldier, TRUE),AI_ACTION_SEEK_OPPONENT,0); + + // sevenfm: avoid going into water, gas or light + if( !TileIsOutOfBounds(pSoldier->aiData.usActionData) && + !Water(pSoldier->aiData.usActionData) && + !InGas( pSoldier, pSoldier->aiData.usActionData ) && + !InLightAtNight( pSoldier->aiData.usActionData, pSoldier->pathing.bLevel ) ) + { + // if soldier can be seen at new position and he cannot be seen at his current position + if ( LocationToLocationLineOfSightTest( pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, tempGridNo, pSoldier->pathing.bLevel, TRUE) && + !LocationToLocationLineOfSightTest( pSoldier->sGridNo, pSoldier->pathing.bLevel, tempGridNo, pSoldier->pathing.bLevel, TRUE) ) + { + // reserve APs for a possible crouch plus a shot + INT32 sCautiousGridNo = InternalGoAsFarAsPossibleTowards(pSoldier, tempGridNo, (INT8) (MinAPsToAttack( pSoldier, tempGridNo, ADDTURNCOST,0) + GetAPsCrouch( pSoldier, TRUE) + GetAPsToLook(pSoldier)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS ); + + if (!TileIsOutOfBounds(sCautiousGridNo)) + { + pSoldier->aiData.usActionData = sCautiousGridNo; + pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_SEEK_OPPONENT); + } + return(AI_ACTION_SEEK_OPPONENT); + } + else + { + return(AI_ACTION_SEEK_OPPONENT); + } + } + else + { + // if we cannot advance to spot, stop trying + pSoldier->numFlanks++; + } + } + else + { + // stop + pSoldier->numFlanks++; + } + } + /*if ( pSoldier->numFlanks == MAX_FLANKS_RED ) { pSoldier->numFlanks += 1; DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: stop flanking"); @@ -3441,8 +3521,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier,pSoldier->aiData.bAIMorale,&iDummy); return AI_ACTION_TAKE_COVER ; } - } - + }*/ // if we can move at least 1 square's worth // and have more APs than we want to reserve @@ -3537,7 +3616,9 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) guiRedSeekCounter++; #endif // if there is an opponent reachable - if (!TileIsOutOfBounds(sClosestDisturbance) && gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_PRONE ) + // sevenfm: allow seeking in prone stance if we haven't seen enemy for several turns + if (!TileIsOutOfBounds(sClosestDisturbance) && + ( gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_PRONE || !GuySawEnemyThisTurnOrBefore(pSoldier) ) ) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: seek opponent"); ////////////////////////////////////////////////////////////////////// @@ -3613,15 +3694,26 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) // return( AI_ACTION_CLIMB_ROOF ); //} + // allow flanking for extra soldiers at night or when overcrowded or no flanking friends yet + BOOLEAN fAllowExtraFlanking = FALSE; + if( NightTime() || + CountNearbyFriendlies(pSoldier, pSoldier->sGridNo, 5) > 2 || + CountFriendsFlankSeek(pSoldier) < CountFriendsInDirection( pSoldier, sClosestDisturbance ) ) + { + fAllowExtraFlanking = TRUE; + } + // possibly start RED flanking - // sevenfm: only CUNNINGAID and CUNNINGSOLO should flank - // check that there are friends between me and noise gridno - // STATIONARY\ONGUARD\CLOSEPATROL\SNIPER should not flank - if ( ( pSoldier->aiData.bAttitude == CUNNINGAID || pSoldier->aiData.bAttitude == CUNNINGSOLO ) && + if ( ( pSoldier->aiData.bAttitude == CUNNINGAID || pSoldier->aiData.bAttitude == CUNNINGSOLO || + ( pSoldier->aiData.bAttitude == BRAVESOLO || pSoldier->aiData.bAttitude == BRAVEAID ) && fAllowExtraFlanking && gGameExternalOptions.fAIExtraFlanking ) && gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_PRONE && - CountFriendsInDirection( pSoldier, sClosestDisturbance ) > 0 && - pSoldier->aiData.bOrders > CLOSEPATROL && - pSoldier->aiData.bOrders != SNIPER ) + CountFriendsInDirection( pSoldier, sClosestDisturbance ) > 1 && + !pSoldier->aiData.bUnderFire && + !GuySawEnemyThisTurnOrBefore( pSoldier ) && + pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && + ( pSoldier->aiData.bOrders == SEEKENEMY || + pSoldier->aiData.bOrders == FARPATROL || + pSoldier->aiData.bOrders == CLOSEPATROL && fAllowExtraFlanking ) ) { INT8 action = AI_ACTION_SEEK_OPPONENT; INT16 dist = PythSpacesAway ( pSoldier->sGridNo, sClosestDisturbance ); @@ -4786,7 +4878,7 @@ INT16 ubMinAPCost; pSoldier->usAISkillUse = SKILLS_RADIO_JAM; pSoldier->aiData.usActionData = skilltargetgridno; return(AI_ACTION_USE_SKILL); - } + } } // VIPs run away (but not the GENERAL) @@ -5162,7 +5254,7 @@ INT16 ubMinAPCost; { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"about to rearrange pocket after punch check"); RearrangePocket(pSoldier,HANDPOS,bWeaponIn,TEMPORARILY); - } + } } } ///////////////////////////////////////////////////////////////////////////////////////////////////// @@ -7749,7 +7841,7 @@ void DecideAlertStatus( SOLDIERTYPE *pSoldier ) DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: couldn't help"); bHelpPts = -99; - } + } } } DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: nothing to do!"); @@ -7806,7 +7898,7 @@ void DecideAlertStatus( SOLDIERTYPE *pSoldier ) #endif return(AI_ACTION_CHANGE_FACING); } - } + } } } @@ -8180,7 +8272,7 @@ void DecideAlertStatus( SOLDIERTYPE *pSoldier ) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"about to rearrange pocket after punch check"); RearrangePocket(pSoldier,HANDPOS,bWeaponIn,TEMPORARILY); - } + } } } ///////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 0f94aba9..f508312a 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -29,6 +29,7 @@ #include "Buildings.h" #include "GameSettings.h" #include "Soldier Profile.h" + #include "rotting corpses.h" // sevenfm #endif @@ -567,6 +568,11 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB UINT8 ubLightPercentDifference; BOOLEAN fNight; + // sevenfm + UINT8 ubNearbyFriends; + BOOLEAN fProneCover; + UINT8 ubDiff = SoldierDifficultyLevel( pSoldier ); + // There's no cover when boxing! if (gTacticalStatus.bBoxingState == BOXING) { @@ -628,7 +634,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB case AGGRESSIVE: iSearchRange -= 2; break; }*/ - // maximum search range is 1 tile / 8 pts of wisdom if (iSearchRange > (pSoldier->stats.bWisdom / 8)) { @@ -787,6 +792,9 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB iCurrentCoverValue = 0; iCurrentScale = 0; + // sevenfm: sight cover + fProneCover = TRUE; + // for every opponent that threatens, consider this spot's cover vs. him for (uiLoop = 0; uiLoop < uiThreatCnt; uiLoop++) { @@ -796,6 +804,14 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // add this opponent's cover value to our current total cover value iCurrentCoverValue += CalcCoverValue(pSoldier,pSoldier->sGridNo,iMyThreatValue,pSoldier->bActionPoints,uiLoop,Threat[uiLoop].iOrigRange,morale,&iCurrentScale); } + // sevenfm: sight test + if( gGameExternalOptions.fAIBetterCover ) + { + if ( SoldierToVirtualSoldierLineOfSightTest( Threat[uiLoop].pOpponent, pSoldier->sGridNo, pSoldier->pathing.bLevel, ANIM_PRONE, TRUE, -1 ) != 0 ) + { + fProneCover = FALSE; + } + } //sprintf(tempstr,"iCurrentCoverValue after opponent %d is now %d",iLoop,iCurrentCoverValue); //PopMessage(tempstr); } @@ -812,6 +828,28 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB iCurrentCoverValue += (iCurrentCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, pSoldier->sGridNo ); } + if( gGameExternalOptions.fAIBetterCover ) + { + // sevenfm: when defending (range change <= 3), prefer locations with sight cover + if( RangeChangeDesire(pSoldier) < 4 ) + { + if( fProneCover ) + { + iCurrentCoverValue += abs(iCurrentCoverValue) / __max(2, 2*RangeChangeDesire(pSoldier)); + } + } + + // sevenfm: check for nearby friends, add bonus/penalty + ubNearbyFriends = __min(5, CountNearbyFriendlies( pSoldier, pSoldier->sGridNo, 5 )); + iCurrentCoverValue -= ubNearbyFriends * abs(iCurrentCoverValue) / (10-ubDiff); + + // sevenfm: penalize locations with fresh corpses + if(GetNearestRottingCorpseAIWarning( pSoldier->sGridNo ) > 0) + { + iCurrentCoverValue -= abs(iCurrentCoverValue) / (8-ubDiff); + } + } + #ifdef DEBUGCOVER // AINumMessage("Search Range = ",iSearchRange); #endif @@ -976,6 +1014,9 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB iCoverValue = 0; iCoverScale = 0; + // sevenfm: check sight cover + fProneCover = TRUE; + // for every opponent that threatens, consider this spot's cover vs. him for (uiLoop = 0; uiLoop < uiThreatCnt; uiLoop++) { @@ -989,6 +1030,15 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB uiLoop,iThreatRange,morale,&iCoverScale); } + // sevenfm: sight test + if( gGameExternalOptions.fAIBetterCover ) + { + if ( SoldierToVirtualSoldierLineOfSightTest( Threat[uiLoop].pOpponent, sGridNo, pSoldier->pathing.bLevel, ANIM_PRONE, TRUE, -1 ) != 0 ) + { + fProneCover = FALSE; + } + } + //sprintf(tempstr,"iCoverValue after opponent %d is now %d",iLoop,iCoverValue); //PopMessage(tempstr); } @@ -1005,6 +1055,26 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB iCoverValue += (iCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, sGridNo ); } + if( gGameExternalOptions.fAIBetterCover ) + { + // sevenfm: when defending (range change <= 3), prefer locations with sight cover + if( RangeChangeDesire(pSoldier) < 4 ) + { + if( fProneCover ) + iCoverValue += abs(iCoverValue) / __max(2, 2*RangeChangeDesire(pSoldier)); + } + + // sevenfm: check for nearby friends in 10 radius, add bonus/penalty 10% + ubNearbyFriends = __min(5, CountNearbyFriendlies( pSoldier, sGridNo, 5 )); + iCoverValue -= ubNearbyFriends * abs(iCoverValue) / (10-ubDiff); + + // sevenfm: penalize locations with fresh corpses + if(GetNearestRottingCorpseAIWarning( sGridNo ) > 0) + { + iCoverValue -= abs(iCoverValue) / (8-ubDiff); + } + } + if ( fNight && !( InARoom( sGridNo, NULL ) ) ) // ignore in buildings in case placed there { // reduce cover at nighttime based on how bright the light is at that location @@ -2545,11 +2615,16 @@ INT32 FindFlankingSpot(SOLDIERTYPE *pSoldier, INT32 sPos, INT8 bAction ) continue; } - // sevenfm: skip water tiles - if( Water( sGridNo ) ) + // sevenfm: allow water flanking only for CUNNINGSOLO soldiers + if( Water( sGridNo ) && pSoldier->aiData.bAttitude != CUNNINGSOLO ) { continue; - //sTempDist = sTempDist/2; + } + + // sevenfm: penalize locations near fresh corpses + if( GetNearestRottingCorpseAIWarning( sGridNo ) > 0 ) + { + sTempDist = sTempDist / 2; } // sevenfm: skip buildings if not in building already, because soldiers often run into buildings and stop flanking @@ -2571,6 +2646,11 @@ INT32 FindFlankingSpot(SOLDIERTYPE *pSoldier, INT32 sPos, INT8 bAction ) { sTempDist = sTempDist / 2; } + // sevenfm: try to flank closer to vision distance limit for faster flanking + if( PythSpacesAway( sGridNo, sPos) > sDistanceVisible + 10 ) + { + sTempDist = sTempDist / 2; + } if ( bAction == AI_ACTION_FLANK_LEFT ) { diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index 6ca66ba2..2bd1db8f 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -276,6 +276,15 @@ UINT8 GetClosestMedicSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTe INT16 MaxNormalVisionDistance( void ); UINT8 CountFriendsInDirection( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo ); BOOLEAN GuySawEnemyThisTurnOrBefore( SOLDIERTYPE * pSoldier ); +BOOLEAN CheckSuppressionDirection( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo ); +UINT8 CountNearbyFriendlies( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ); +UINT8 CountNearbyFriendliesLastAttackHit( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ); +UINT8 CountFriendsFlankSeek( SOLDIERTYPE *pSoldier ); +UINT8 CountNearbyFriendliesContact( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ); +UINT8 CountNearbyFriendliesNoContact( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ); +BOOLEAN AICheckFriendsNoContact( SOLDIERTYPE *pSoldier ); + +INT8 CalcMoraleNew(SOLDIERTYPE *pSoldier); // moved from DecideAction.cpp // sevenfm: set MAX_FLANKS_RED and MAX_FLANKS_YELLOW to equal values to avoid problems when soldier's alert state changes @@ -283,15 +292,11 @@ BOOLEAN GuySawEnemyThisTurnOrBefore( SOLDIERTYPE * pSoldier ); #define MAX_FLANKS_YELLOW 25 // limit min/max flank distance depending on sight range and time of day -//#define MIN_FLANK_DIST_YELLOW 10 * STRAIGHT_RATIO -//#define MAX_FLANK_DIST_YELLOW 50 * STRAIGHT_RATIO -#define MIN_FLANK_DIST_YELLOW (2*MaxNormalVisionDistance()/3) +#define MIN_FLANK_DIST_YELLOW (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO) #define MAX_FLANK_DIST_YELLOW (MaxNormalVisionDistance() + 20) // limit min/max flank distance depending on sight range and time of day -//#define MIN_FLANK_DIST_RED 10 * STRAIGHT_RATIO -//#define MAX_FLANK_DIST_RED 40 * STRAIGHT_RATIO -#define MIN_FLANK_DIST_RED (2*MaxNormalVisionDistance()/3) +#define MIN_FLANK_DIST_RED (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO) #define MAX_FLANK_DIST_RED (MaxNormalVisionDistance() + 20) #endif diff --git a/Utils/_RussianText.cpp b/Utils/_RussianText.cpp index 1c560618..6e9dc00e 100644 --- a/Utils/_RussianText.cpp +++ b/Utils/_RussianText.cpp @@ -8852,7 +8852,7 @@ STR16 szBackgroundText_Value[]= L" %s%d%% сила\n", L" %s%d%% лидерство\n", L" %s%d%% меткость\n", - L" %s%d%% маханика\n", + L" %s%d%% механика\n", L" %s%d%% взрывчатка\n", L" %s%d%% медицина\n", L" %s%d%% мудрость\n",