From dcf8b3abaf234108e08fd3e27078829f36a9adff Mon Sep 17 00:00:00 2001 From: silversurfer Date: Sat, 19 Dec 2015 14:22:47 +0000 Subject: [PATCH] AI improvements (by Sevenfm): Bugfixes: - Fix: Watched Locations system (never worked in 1.13) - Fix: LineOfSight test with NO_DISTANCE_LIMIT (stopped on trees previously) AI/tactical improvements: - EstimateThrowDamage additionally calculates damage from fragments (important for AI to use frag grenades with low main damage and many fragments) - EstimateThrowDamage correctly adds IndoorModifier - Additional checks for RED suppression (check that the soldier will not shoot his friends, check that he has extra clip to reload weapon, check that target is not cowering, don't suppress when flanking) - ClosestReachableDisturbance will use general noise only if cannot find any enemy - Allow RED seeking in prone stance if we haven't seen enemy for several turns Flanking improvements: - allow YELLOW flanking for CLOSEPATROL at night - better YELLOW/RED flanking start/stop conditions - better RED AI behavior when finished flanking (try to reach flanking spot) - allow RED flanking for CLOSEPATROL soldiers when at night, overcrowded or no other flanking soldiers yet Optional AI improvements: AI_EXTRA_SUPPRESSION - AI will use suppression fire more often in RED state (set minimum AI CTH to 1) AI_NEW_MORALE - Better dynamic morale calculation depending on tactical situation. - Additional min/max limits for AI morale - Disabled HOPELESS morale for soldiers without guns (they can pick up weapon or attack with hands sometimes) AI_BETTER_COVER - Additional checks for line of sight, fresh corpses and overcrowded locations AI_EXTRA_FLANKING - Allow flanking for BRAVEAID/BRAVESOLO git-svn-id: https://ja2svn.mooo.com/source/ja2/trunk/GameSource/ja2_v1.13/Build@8006 3b4a5df2-a311-0410-b5c6-a8a6f20db521 --- GameSettings.cpp | 6 + GameSettings.h | 4 + GameVersion.cpp | 18 +- Tactical/LOS.cpp | 5 +- Tactical/Weapons.cpp | 12 + Tactical/opplist.cpp | 6 +- TacticalAI/AIUtils.cpp | 512 ++++++++++++++++++++++++++++++++++- TacticalAI/Attacks.cpp | 14 + TacticalAI/DecideAction.cpp | 158 ++++++++--- TacticalAI/FindLocations.cpp | 88 +++++- TacticalAI/ai.h | 17 +- Utils/_RussianText.cpp | 2 +- 12 files changed, 783 insertions(+), 59 deletions(-) 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",